UNPKG

toosoon-lsystem

Version:

Library providing functionalities for creating and manipulating Lindenmayer systems (L-Systems) using various parameters

297 lines (296 loc) 11.1 kB
import { PRNG } from 'toosoon-prng'; import { DEFAULT_SYMBOLS, IGNORED_SYMBOLS } from './constants'; import { matchContext } from './utils'; import { normalizeAxiom, normalizeProduction, transformParamsToDefines, transformPhraseToAxiom } from './transformers'; /** * LSystem class * * @exports * @class LSystem * @template {Alphabet} [A=DefaultAlphabet] * @template {Alphabet} [I=IgnoredAlphabet] */ export default class LSystem { alphabet; ignoredSymbols; axiom = []; iterations; defines = new Map(); productions = new Map(); commands = new Map(); _prng; /** * @param {LSystemParameters<A,I>} params */ constructor({ alphabet = [...DEFAULT_SYMBOLS], ignoredSymbols = [...IGNORED_SYMBOLS], axiom = '', iterations = 1, defines, productions, commands, seed, generator }) { this.alphabet = alphabet; this.ignoredSymbols = ignoredSymbols; this.setAxiom(axiom); this.iterations = Math.floor(iterations); if (defines) this.setDefines(defines); if (productions) this.setProductions(productions); if (commands) this.setCommands(commands); this._prng = new PRNG(seed, generator); } /** * Set the axiom of the L-System * * @param {AxiomParameter<A|I>} axiom Initial phrase of this L-System */ setAxiom(axiom) { this.axiom = normalizeAxiom(axiom, this.alphabet, this.ignoredSymbols, this.defines); } /** * Set a define for this L-System * * @param {DefineKey} key Key for defining constant * @param {Define} define A constant value */ setDefine(key, define) { this.defines.set(key, define); } /** * Set multiple defines for the L-System. * * @param {object} defines Collection of defined constants */ setDefines(defines) { this.clearDefines(); Object.entries(defines).forEach(([key, define]) => this.setDefine(key, define)); } /** * Clear all defines from this L-System */ clearDefines() { this.defines.clear(); } /** * Set a production for the L-System. * * @param {SuccessorParameter<A>} successorParameter Successor symbol mapped to the production * @param {ProductionParameter<A,I>} productionParameter Production rule mapped to the symbol */ setProduction(successorParameter, productionParameter) { // Apply transformers and normalizations const { symbol, production } = normalizeProduction(successorParameter, productionParameter); if (this.productions.has(symbol)) { // Add new production to array if other productions are already associated to this symbol let existingProduction = this.productions.get(symbol); // TODO: Compare productions context and merge/replace existing production if (!(existingProduction instanceof Array)) { existingProduction = [existingProduction]; } existingProduction.push(production); this.productions.set(symbol, existingProduction); } else { // Set new production if this symbol has no associated production yet this.productions.set(symbol, production); } } /** * Set multiple productions for this L-System * * @param {object} productions Collection of production rules mapped to symbols */ setProductions(productions) { this.clearProductions(); Object.entries(productions).forEach(([successorParameter, productionParameter]) => this.setProduction(successorParameter, productionParameter)); } /** * Clear all productions from the L-System */ clearProductions() { this.productions.clear(); } /** * Return the result of a production rule * * @param {Production<A,I>} production * @param {AxiomPart<A|I>} part * @param {number} index * @param {boolean} [recursive=false] * @returns {ProductionResult<A|I>} */ _getProductionResult(production, part, index, recursive = false) { let result = false; let precheck = true; // TODO: Eval string for condition if ( // Check if condition is true production.condition !== undefined && production.condition({ axiom: this.axiom, index, part, params: part.params ?? [] }) === false) { precheck = false; } else if (production.context?.before !== undefined || production.context?.after !== undefined) { // Check left and right contexts const contextParameters = { axiom: this.axiom, index: index, alphabet: this.alphabet, defines: this.defines, ignoredSymbols: this.ignoredSymbols }; if (production.context?.before && production.context?.after) { precheck = matchContext({ direction: 'before', match: production.context.before, ...contextParameters }) && matchContext({ direction: 'after', match: production.context.after, ...contextParameters }); } else if (production.context?.before) { precheck = matchContext({ direction: 'before', match: production.context.before, ...contextParameters }); } else if (production.context?.after) { precheck = matchContext({ direction: 'after', match: production.context.after, ...contextParameters }); } } if (precheck === false) { // If conditions and context don't allow production, keep result = false result = false; } else if (production.stochastic) { // For stochastic productions pick a successor from the list according to their weight const subseed = `${part.symbol}${index}`; const weights = production.stochastic.map((item) => item.weight); const item = production.stochastic[this._prng.randomIndex(subseed, weights)]; result = this._getProductionResult({ successor: item.successor }, part, index); } else if (typeof production.successor === 'string') { // If parameter is a Phrase, transform and merge it into new axiom // Merge production params (from classic parametric syntax) to global defines const defines = new Map(); this.defines.forEach((value, key) => defines.set(key, value)); transformParamsToDefines(production.params, part.params).forEach((value, key) => defines.set(key, value)); result = transformPhraseToAxiom(production.successor, this.alphabet, this.ignoredSymbols, defines); } else if (typeof production.successor === 'function') { // If successor is a function, execute function and append returned value result = production.successor({ axiom: this.axiom, index, part, params: part.params ?? [] }) ?? false; } else if (production.successor instanceof Array) { // If successor is an Axiom array, return value result = production.successor; } // Allow false results for recursive calls if (!result) { return recursive ? result : part; } return result; } /** * Apply productions rules on current axiom. * It corresponds to 1 iteration of this L-System. * * @returns {Axiom<A|I>} */ _applyProductions() { let axiom = []; let index = 0; // Iterate all symbols of the axiom and lookup according productions this.axiom.forEach((part) => { const symbol = part.symbol; let productionResult = part; if (this.productions.has(symbol)) { const production = this.productions.get(symbol); if (production instanceof Array) { // If symbol has multiple productions associated, set first valid result for (let productionItem of production) { let result = this._getProductionResult(productionItem, part, index, true); if (result) { productionResult = result; break; } } } else { // If symbol has only one production associated, set result productionResult = this._getProductionResult(production, part, index); } } // Add result to new axiom axiom.push(...normalizeAxiom(productionResult, this.alphabet, this.ignoredSymbols, this.defines)); index++; }); return axiom; } /** * Set a command for this L-System * * @param {Symbol<A|I>} symbol Symbol used as a key for the command * @param {Command<A,I>} command Function to be executed for each corresponding symbol */ setCommand(symbol, command) { this.commands.set(symbol, command); } /** * Set multiple commands for this L-System * * @param {object} commands Collection of commands mapped to symbols */ setCommands(commands) { this.clearCommands(); Object.entries(commands).forEach(([key, command]) => this.setCommand(key, command)); } /** * Clear all commands from this L-System */ clearCommands() { this.commands.clear(); } /** * Execute the commands defined in this L-System */ run() { let index = 0; // Execute commands this.axiom.forEach((part) => { const symbol = part.symbol; if (this.commands.has(symbol)) { const command = this.commands.get(symbol); command({ index, part, params: part.params ?? [] }); } index++; }); } /** * Perform a specified number of iterations on this L-System * * @param {number} [iterations] Number of iterations * @returns {Axiom<A|I>} */ iterate(iterations = this.iterations) { this.iterations = Math.floor(iterations); for (let i = 0; i < iterations; i++) { this.axiom = this._applyProductions(); } return this.axiom; } /** * Get the current axiom of this L-System * * @returns {string} */ getAxiomString() { if (typeof this.axiom === 'string') return this.axiom; return this.axiom.reduce((prev, current) => prev + current.symbol, ''); } }