Skip to content

Instantly share code, notes, and snippets.

@pkrumins
Created June 20, 2019 20:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pkrumins/ab58506c225d1c388b85665609026a9e to your computer and use it in GitHub Desktop.
Save pkrumins/ab58506c225d1c388b85665609026a9e to your computer and use it in GitHub Desktop.
nylki/lindenmayer library that works in IE
/* This is a modified version of https://github.com/nylki/lindenmayer.
** The modifications make it work in Internet Explorer again.
** The modifications include:
** * Replacing `let` with `var`.
** * Replacing `{ x }` with `{ x : x }` in object context.
** * Replacing `class` with `function.
** * Replacing `{ arg1, arg2, }` with `opts` in function definition context.
** * Replacing `x of obj` with a `for (var i = 0; i < obj.length; i++)`
** * Replacing default arguments with `if (arg === undefined) { arg = defaultVal; }`
** * Replacing `array.push(...obj)` with `array.extend(obj)`.
** * Adding `Object.entries` polyfill.
*/
'use strict';
if (!Object.entries) {
Object.entries = function( obj ){
var ownProps = Object.keys( obj ),
i = ownProps.length,
resArray = new Array(i); // preallocate the Array
while (i--)
resArray[i] = [ownProps[i], obj[ownProps[i]]];
return resArray;
};
}
function LSystem (opts) {
var axiom = opts.axiom;
if (axiom === undefined) {
axiom = '';
}
var productions = opts.productions;
var finals = opts.finals;
var branchSymbols = opts.branchSymbols;
if (branchSymbols === undefined) {
branchSymbols = '[]';
}
var ignoredSymbols = opts.ignoredSymbols;
if (ignoredSymbols === undefined) {
ignoredSymbols = '+-&^/|\\';
}
var allowClassicSyntax = opts.allowClassicSyntax;
if (allowClassicSyntax === undefined) {
allowClassicSyntax = true;
}
var classicParametricSyntax = opts.classicParametricSyntax;
if (classicParametricSyntax === undefined) {
classicParametricSyntax = false;
}
var forceObjects = opts.forceObjects;
if (forceObjects === undefined) {
forceObjects = false;
}
var debug = opts.debug;
if (debug === undefined) {
debug = false;
}
var self = this;
self.ignoredSymbols = ignoredSymbols;
self.debug = debug;
self.branchSymbols = branchSymbols;
self.allowClassicSyntax = allowClassicSyntax;
self.classicParametricSyntax = classicParametricSyntax;
self.forceObjects = forceObjects;
// TODO: forceObject to be more intelligent based on other productions??
function setAxiom(axiom) {
self.axiom = self.forceObjects ? stringToObjects(axiom) : axiom;
}
self.setAxiom = setAxiom;
function getRaw() {
return self.axiom;
}
self.getRaw = getRaw;
// if using objects in axioms, as used in parametric L-Systems
function getString(onlySymbols) {
if (onlySymbols === undefined) {
onlySymbols = true;
}
if (typeof self.axiom === 'string') return self.axiom;
if (onlySymbols === true) {
return self.axiom.reduce(function (prev, current) {
if (current.symbol === undefined) {
console.log('found:', current);
throw new Error('L-Systems that use only objects as symbols (eg: {symbol: \'F\', params: []}), cant use string symbols (eg. \'F\')! Check if you always return objects in your productions and no strings.');
}
return prev + current.symbol;
}, '');
} else {
return JSON.stringify(self.axiom);
}
}
self.getString = getString;
function setProduction(from, to, allowAppendingMultiSuccessors) {
if (allowAppendingMultiSuccessors === undefined) {
allowAppendingMultiSuccessors = false;
}
var newProduction = [from, to];
if (newProduction === undefined) {
throw new Error('no production specified.');
}
if (to.successor && to.successors) {
throw new Error('You can not have both a "successor" and a "successors" field in your production!');
}
// Apply production transformers and normalizations
if (self.allowClassicSyntax === true) {
newProduction = transformClassicCSProduction(newProduction);
}
newProduction = normalizeProduction(newProduction, self.forceObjects);
// check wether production is stochastic
newProduction[1].isStochastic = newProduction[1].successors !== undefined &&
newProduction[1].successors.every(function (successor) { return successor.weight !== undefined });
if (newProduction[1].isStochastic) {
// calculate weight sum
newProduction[1].weightSum = 0;
for (var i = 0; i < newProduction[1].successors.length; i++) {
var s = newProduction[1].successors[i];
newProduction[1].weightSum += s.weight;
}
}
var symbol = newProduction[0];
if (allowAppendingMultiSuccessors === true && self.productions.has(symbol)) {
var existingProduction = self.productions.get(symbol);
var singleSuccessor = existingProduction.successor;
var multiSuccessors = existingProduction.successors;
if (singleSuccessor && !multiSuccessors) {
// replace existing prod with new obj and add previous successor as first elem
// to new successors field.
existingProduction = { successors: [existingProduction] };
}
existingProduction.successors.push(newProduction[1]);
self.productions.set(symbol, existingProduction);
} else {
self.productions.set(symbol, newProduction[1]);
}
}
self.setProduction = setProduction;
// set multiple productions from name:value Object
function setProductions(newProductions) {
if (newProductions === undefined) throw new Error('no production specified.');
self.clearProductions();
var entries = Object.entries(newProductions);
for (var i = 0; i < entries.length; i++) {
var _ref = entries[i];
var from = _ref[0];
var to = _ref[1];
self.setProduction(from, to, true);
}
}
self.setProductions = setProductions;
function clearProductions() {
self.productions = new Map();
}
self.clearProductions = clearProductions;
function setFinal(symbol, final) {
var newFinal = [symbol, final];
if (newFinal === undefined) {
throw new Error('no final specified.');
}
self.finals.set(newFinal[0], newFinal[1]);
}
self.setFinal = setFinal;
// set multiple finals from name:value Object
function setFinals(newFinals) {
if (newFinals === undefined) throw new Error('no finals specified.');
self.finals = new Map();
for (var symbol in newFinals) {
if (newFinals.hasOwnProperty(symbol)) {
self.setFinal(symbol, newFinals[symbol]);
}
}
}
self.setFinals = setFinals;
//var hasWeight = el => el.weight !== undefined;
function getProductionResult(p, index, part, params, recursive) {
if (recursive === undefined) {
recursive = false;
}
var contextSensitive = p.leftCtx !== undefined || p.rightCtx !== undefined;
var conditional = p.condition !== undefined;
var stochastic = false;
var result = false;
var precheck = true;
// Check if condition is true, only then continue to check left and right contexts
if (conditional && p.condition({ index : index, currentAxiom: self.axiom, part : part, params : params }) === false) {
precheck = false;
} else if (contextSensitive) {
if (p.leftCtx !== undefined && p.rightCtx !== undefined) {
precheck = self.match({
direction: 'left',
match: p.leftCtx,
index: index,
branchSymbols: self.branchSymbols,
ignoredSymbols: self.ignoredSymbols
}).result && self.match({
direction: 'right',
match: p.rightCtx,
index: index,
branchSymbols: self.branchSymbols,
ignoredSymbols: self.ignoredSymbols
}).result;
} else if (p.leftCtx !== undefined) {
precheck = self.match({
direction: 'left',
match: p.leftCtx,
index: index,
branchSymbols: self.branchSymbols,
ignoredSymbols: self.ignoredSymbols
}).result;
} else if (p.rightCtx !== undefined) {
precheck = self.match({
direction: 'right',
match: p.rightCtx,
index: index,
branchSymbols: self.branchSymbols,
ignoredSymbols: self.ignoredSymbols
}).result;
}
}
// If conditions and context don't allow product, keep result = false
if (precheck === false) {
result = false;
}
// If p has multiple successors
else if (p.successors) {
// This could be stochastic successors or multiple functions
// Treat every element in the list as an individual production object
// For stochastic productions (if all prods in the list have a 'weight' property)
// Get a random number then pick a production from the list according to their weight
var currentWeight, threshWeight;
if (p.isStochastic) {
threshWeight = Math.random() * p.weightSum;
currentWeight = 0;
}
/*
go through the list and use
the first valid production in that list. (that returns true)
This assumes, it's a list of functions.
No recursion here: no successors inside successors.
*/
for (var i = 0; i < p.successors.length; i++) {
var _p = p.successors[i];
if (p.isStochastic) {
currentWeight += _p.weight;
if (currentWeight < threshWeight) continue;
}
// If currentWeight >= thresWeight, a production is choosen stochastically
// and evaluated recursively because it , kax also have rightCtx, leftCtx and condition to further inhibit production. This is not standard L-System behaviour though!
// last true is for recursiv call
// TODO: refactor getProductionResult to use an object if not a hit on perf
var _result = self.getProductionResult(_p, index, part, params, true);
if (_result !== undefined && _result !== false) {
result = _result;
break;
}
}
}
// if successor is a function, execute function and append return value
else if (typeof p.successor === 'function') {
result = p.successor({ index : index, currentAxiom: self.axiom, part : part, params : params });
} else {
result = p.successor;
}
if (!result) {
// Allow undefined or false results for recursive calls of this func
return recursive ? result : part;
}
return result;
}
self.getProductionResult = getProductionResult;
function applyProductions() {
// a axiom can be a string or an array of objects that contain the key/value 'symbol'
var newAxiom = typeof self.axiom === 'string' ? '' : [];
var index = 0;
// iterate all symbols/characters of the axiom and lookup according productions
for (var i = 0; i < self.axiom.length; i++) {
var part = self.axiom[i];
// Stuff for classic parametric L-Systems: get actual symbol and possible parameters
// params will be given the production function, if applicable.
var symbol = part.symbol || part;
var params = part.params || [];
var result = part;
if (self.productions.has(symbol)) {
var p = self.productions.get(symbol);
result = self.getProductionResult(p, index, part, params);
}
// Got result. Now add result to new axiom.
if (typeof newAxiom === 'string') {
newAxiom += result;
} else if (result instanceof Array) {
// If result is an array, merge result into new axiom instead of pushing.
newAxiom.concat(result);
} else {
newAxiom.push(result);
}
index++;
}
// finally set new axiom and also return it for convenience.
self.axiom = newAxiom;
return newAxiom;
}
self.applyProductions = applyProductions;
function iterate(n) {
if (n === undefined) {
n = 1;
}
self.iterations = n;
var lastIteration;
for (var iteration = 0; iteration < n; iteration++) {
lastIteration = self.applyProductions();
}
return lastIteration;
}
self.iterate = iterate;
function final(externalArg) {
var index = 0;
for (var i = 0; i < self.axiom.length; i++) {
var part = self.axiom[i];
// if we have objects for each symbol, (when using parametric L-Systems)
// get actual identifiable symbol character
var symbol = part;
if (typeof part === 'object' && part.symbol) symbol = part.symbol;
if (self.finals.has(symbol)) {
var finalFunction = self.finals.get(symbol);
var typeOfFinalFunction = typeof finalFunction;
if (typeOfFinalFunction !== 'function') {
throw Error('\'' + symbol + '\'' + ' has an object for a final function. But it is __not a function__ but a ' + typeOfFinalFunction + '!');
}
// execute symbols function
// supply in first argument an details object with current index and part
// and in the first argument inject the external argument (like a render target)
finalFunction({ index : index, part : part }, externalArg);
} else {
// symbol has no final function
}
index++;
}
}
self.final = final;
/*
how to use match():
-----------------------
It is mainly a helper function for context sensitive productions.
If you use the classic syntax, it will by default be automatically transformed to proper
JS-Syntax.
Howerver, you can use the match helper function in your on productions:
index is the index of a production using `match`
eg. in a classic L-System
LSYS = ABCDE
B<C>DE -> 'Z'
the index of the `B<C>D -> 'Z'` production would be the index of C (which is 2) when the
production would perform match(). so (if not using the ClassicLSystem class) you'd construction your context-sensitive production from C to Z like so:
LSYS.setProduction('C', (index, axiom) => {
(LSYS.match({index, match: 'B', direction: 'left'}) &&
LSYS.match({index, match: 'DE', direction: 'right'}) ? 'Z' : 'C')
})
You can just write match({index, ...} instead of match({index: index, ..}) because of new ES6 Object initialization, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#New_notations_in_ECMAScript_6
*/
function match(opts) {
var axiom_ = opts.axiom_;
var match = opts.match;
var ignoredSymbols = opts.ignoredSymbols;
var index = opts.index;
var direction = opts.direction;
var branchCount = 0;
var explicitBranchCount = 0;
axiom_ = axiom_ || self.axiom;
if (branchSymbols === undefined) branchSymbols = self.branchSymbols !== undefined ? self.branchSymbols : [];
if (ignoredSymbols === undefined) ignoredSymbols = self.ignoredSymbols !== undefined ? self.ignoredSymbols : [];
var returnMatchIndices = [];
var branchStart, branchEnd, axiomIndex, loopIndexChange, matchIndex, matchIndexChange, matchIndexOverflow;
// set some variables depending on the direction to match
if (direction === 'right') {
loopIndexChange = matchIndexChange = +1;
axiomIndex = index + 1;
matchIndex = 0;
matchIndexOverflow = match.length;
if (branchSymbols.length > 0) {
var _branchSymbols = branchSymbols;
branchStart = _branchSymbols[0];
branchEnd = _branchSymbols[1];
}
} else if (direction === 'left') {
loopIndexChange = matchIndexChange = -1;
axiomIndex = index - 1;
matchIndex = match.length - 1;
matchIndexOverflow = -1;
if (branchSymbols.length > 0) {
var _branchSymbols2 = branchSymbols;
branchEnd = _branchSymbols2[0];
branchStart = _branchSymbols2[1];
}
} else {
throw Error(direction, 'is not a valid direction for matching.');
}
for (; axiomIndex < axiom_.length && axiomIndex >= 0; axiomIndex += loopIndexChange) {
var axiomSymbol = axiom_[axiomIndex].symbol || axiom_[axiomIndex];
var matchSymbol = match[matchIndex];
// compare current symbol of axiom with current symbol of match
if (axiomSymbol === matchSymbol) {
if (branchCount === 0 || explicitBranchCount > 0) {
// if its a match and previously NOT inside branch (branchCount===0) or in explicitly wanted branch (explicitBranchCount > 0)
// if a bracket was explicitly stated in match axiom
if (axiomSymbol === branchStart) {
explicitBranchCount++;
branchCount++;
matchIndex += matchIndexChange;
} else if (axiomSymbol === branchEnd) {
explicitBranchCount = Math.max(0, explicitBranchCount - 1);
branchCount = Math.max(0, branchCount - 1);
// only increase match if we are out of explicit branch
if (explicitBranchCount === 0) {
matchIndex += matchIndexChange;
}
} else {
returnMatchIndices.push(axiomIndex);
matchIndex += matchIndexChange;
}
}
// overflowing matchIndices (matchIndex + 1 for right match, matchIndexEnd for left match )?
// -> no more matches to do. return with true, as everything matched until here
// *yay*
if (matchIndex === matchIndexOverflow) {
return { result: true, matchIndices: returnMatchIndices };
}
} else if (axiomSymbol === branchStart) {
branchCount++;
if (explicitBranchCount > 0) explicitBranchCount++;
} else if (axiomSymbol === branchEnd) {
branchCount = Math.max(0, branchCount - 1);
if (explicitBranchCount > 0) explicitBranchCount = Math.max(0, explicitBranchCount - 1);
} else if ((branchCount === 0 || explicitBranchCount > 0 && matchSymbol !== branchEnd) && ignoredSymbols.includes(axiomSymbol) === false) {
// not in branchSymbols/branch? or if in explicit branch, and not at the very end of
// condition (at the ]), and symbol not in ignoredSymbols ? then false
return { result: false, matchIndices: returnMatchIndices };
}
}
return { result: false, matchIndices: returnMatchIndices };
}
self.match = match;
// Get a list of productions that have identical initiators,
// Output a single stochastic production. Probability per production
// is defined by amount of input productions (4 => 25% each, 2 => 50% etc.)
// These transformers get a classic ABOP snytax as input and return a standardized
// production object in the form of ['F',
// {
// successor:String/Iterable
// [alternatively]stochasticSuccessors: Iterable of standardized objects with mandatory weight fields,
// leftCtx: iterable/string,
// rightCtx: Iterable/String,
// condition: Function }]
function transformClassicStochasticProductions(productions) {
return function transformedProduction() {
var resultList = productions; // the parser for productions shall create this list
var count = resultList.length;
var r = Math.random();
for (var i = 0; i < count; i++) {
var range = (i + 1) / count;
if (r <= range) return resultList[i];
}
console.error('Should have returned a result of the list, something is wrong here with the random numbers?.');
};
}
// TODO: implement it!
// TODO: Scaffold classic parametric and context sensitive stuff out of main file
// And simply require it here, eg:
// this.testClassicParametricSyntax = require(classicSyntax.testParametric)??
function testClassicParametricSyntax(axiom) {
return (/\(.+\)/.test(axiom)
);
}
// transforms things like 'A(1,2,5)B(2.5)' to
// [ {symbol: 'A', params: [1,2,5]}, {symbol: 'B', params:[25]} ]
// strips spaces
function transformClassicParametricAxiom(axiom) {
// Replace whitespaces, then split between square brackets.
var splitAxiom = axiom.replace(/\s+/g, '').split(/[\(\)]/);
// console.log('parts:', splitAxiom)
var newAxiom = [];
// Construct new axiom by getting the params and symbol.
for (var i = 0; i < splitAxiom.length - 1; i += 2) {
var params = splitAxiom[i + 1].split(',').map(Number);
newAxiom.push({ symbol: splitAxiom[i], params: params });
}
// console.log('parsed axiom:', newAxiom)
}
function transformClassicCSProduction(p) {
// before continuing, check if classic syntax actually there
// example: p = ['A<B>C', 'Z']
// left should be ['A', 'B']
var left = p[0].match(/(.+)<(.)/);
// right should be ['B', 'C']
var right = p[0].match(/(.)>(.+)/);
// Not a CS-Production (no '<' or '>'),
//return original production.
if (left === null && right === null) {
return p;
}
var predecessor;
// create new production object _or_ use the one set by the user
var productionObject = p[1].successor || p[1].successors ? p[1] : { successor: p[1] };
if (left !== null) {
predecessor = left[2];
productionObject.leftCtx = left[1];
}
if (right !== null) {
predecessor = right[1];
productionObject.rightCtx = right[2];
}
return [predecessor, productionObject];
}
function stringToObjects(string) {
console.log('x');
if (typeof string !== 'string' && string instanceof String === false) return string;
var transformed = [];
for (var i = 0; i < string.length; i++) {
transformed.push({ string : string[i] });
}
return transformed;
}
// TODO: continue here
// transform p to {successor: p}
// if applicable also transform strings into array of {symbol: String} objects
// TODO: make more modular! dont have forceObjects in here
function normalizeProductionRightSide(p, forceObjects) {
if (p.hasOwnProperty('successors')) {
for (var i = 0; i < p.successors.length; i++) {
p.successors[i] = normalizeProductionRightSide(p.successors[i], forceObjects);
}
} else if (p.hasOwnProperty('successor') === false) {
p = { successor: p };
}
if (forceObjects && p.hasOwnProperty('successor')) {
p.successor = stringToObjects(p.successor);
}
return p;
}
function normalizeProduction(p, forceObjects) {
p[1] = normalizeProductionRightSide(p[1], forceObjects);
return p;
}
self.getStringResult = getString;
// Set classic syntax helpers to library scope to be used outside of library context
// for users eg.
self.transformClassicStochasticProductions = transformClassicStochasticProductions;
self.transformClassicCSProduction = transformClassicCSProduction;
self.transformClassicParametricAxiom = transformClassicParametricAxiom;
self.testClassicParametricSyntax = testClassicParametricSyntax;
self.setAxiom(axiom);
self.clearProductions();
if (productions) self.setProductions(productions);
if (finals) self.setFinals(finals);
return self;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment