UNPKG

aframe-lsystem-component

Version:

L-System/LSystem component for A-Frame to draw 3D turtle graphics. Using Lindenmayer as backend.

1,410 lines (1,169 loc) 79.4 kB
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lindenmayer__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_worker_loader_inline_fallback_false_worker_js__ = __webpack_require__(2); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_worker_loader_inline_fallback_false_worker_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_worker_loader_inline_fallback_false_worker_js__); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__primitives_a_lsystem_js__ = __webpack_require__(4); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__primitives_a_lsystem_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2__primitives_a_lsystem_js__); var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } // As we use webpack for compiling the source, it's used to bundle the // web worker into a blob via: https://github.com/webpack/worker-loader /** * Lindenmayer-System component for A-Frame. */ function parseFromTo(value) { let flatResult = value.split(/(\w)\s*:\s*/).filter(part => part.length !== 0); let result = []; for (var i = 0; i < flatResult.length; i += 2) { result.push([flatResult[i], flatResult[i + 1]]); } return result; } AFRAME.registerComponent('lsystem', { schema: { axiom: { type: 'string', default: 'F' }, productions: { default: 'F:FF', // return an array of production tuples ([[from, to], ['F', 'F+F']]) parse: value => parseFromTo(value).map((_ref) => { var _ref2 = _slicedToArray(_ref, 2); let from = _ref2[0], to = _ref2[1]; return [from, to.replace(/\s/g, '')]; }) }, // A: blue line, red line, yellow line B: red line segmentMixins: { type: 'string', parse: function parse(value) { let mixinsForSymbol = new Map(); let result = parseFromTo(value); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = result[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { let _ref3 = _step.value; var _ref4 = _slicedToArray(_ref3, 2); let from = _ref4[0]; let to = _ref4[1]; to = to.replace(/[\[\]]/g, '').split(','); mixinsForSymbol.set(from, to); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return mixinsForSymbol; } }, iterations: { type: 'int', default: 1 }, angle: { default: 90.0 }, translateAxis: { type: 'string', default: 'y', parse: function parse(value) { value = value.toLowerCase(); if (value === 'x') { return new THREE.Vector3(1, 0, 0); } else if (value === 'y') { return new THREE.Vector3(0, 1, 0); } else if (value === 'z') { return new THREE.Vector3(0, 0, 1); } else { throw new Error('translateAxis has to be a string: "x", "y" or "z"'); } } }, scaleFactor: { default: 1.0 }, dynamicSegmentLength: { default: true }, mergeGeometries: { type: 'boolean', default: true }, functionsInProductions: { type: 'boolean', default: true } }, /** * Called once when component is attached. Generally for initial setup. */ init: function init() { this.sceneEl = document.querySelector('a-scene'); let self = this; this.initWorker(); this.X = new THREE.Vector3(1, 0, 0); this.Y = new THREE.Vector3(0, 1, 0); this.Z = new THREE.Vector3(0, 0, 1); this.xPosRotation = new THREE.Quaternion(); this.xNegRotation = new THREE.Quaternion(); this.yPosRotation = new THREE.Quaternion(); this.yNegRotation = new THREE.Quaternion(); this.zPosRotation = new THREE.Quaternion(); this.zNegRotation = new THREE.Quaternion(); this.yReverseRotation = new THREE.Quaternion(); this.xPosRotation = new THREE.Quaternion(); this.xNegRotation = new THREE.Quaternion(); this.yPosRotation = new THREE.Quaternion(); this.yNegRotation = new THREE.Quaternion(); this.zPosRotation = new THREE.Quaternion(); this.zNegRotation = new THREE.Quaternion(); this.yReverseRotation = new THREE.Quaternion(); this.segmentLengthFactor = 1.0; this.transformationSegment = new THREE.Object3D(); this.transformationSegmentTemplate = this.transformationSegment.clone(); let scaleFactor = self.data.scaleFactor; this.colorIndex = 0; this.lineWidth = 0.0005; this.lineLength = 0.125; this.LSystem = new __WEBPACK_IMPORTED_MODULE_0_lindenmayer__["a" /* default */]({ axiom: 'F', productions: { 'F': 'F' }, finals: { /* As a default F is already defined as final, new ones get added automatically by parsing the segment mixins. If no segment mixin for any symbol is defined it wont get a final function and therefore not render. */ '+': () => { self.transformationSegment.quaternion.multiply(self.yPosRotation); }, '-': () => { self.transformationSegment.quaternion.multiply(self.yNegRotation); }, '&': () => { self.transformationSegment.quaternion.multiply(self.zNegRotation); }, '^': () => { self.transformationSegment.quaternion.multiply(self.zPosRotation); }, '\\': () => { self.transformationSegment.quaternion.multiply(self.xNegRotation); }, '<': () => { self.transformationSegment.quaternion.multiply(self.xNegRotation); }, '/': () => { self.transformationSegment.quaternion.multiply(self.xPosRotation); }, '>': () => { self.transformationSegment.quaternion.multiply(self.xPosRotation); }, '|': () => { self.transformationSegment.quaternion.multiply(self.yReverseRotation); }, '!': () => { self.segmentLengthFactor *= scaleFactor; self.transformationSegment.scale.set(self.transformationSegment.scale.x *= scaleFactor, self.transformationSegment.scale.y *= scaleFactor, self.transformationSegment.scale.z *= scaleFactor); self.colorIndex++; }, '\'': () => { self.segmentLengthFactor *= 1.0 / scaleFactor; self.transformationSegment.scale.set(self.transformationSegment.scale.x *= 1.0 / scaleFactor, self.transformationSegment.scale.y *= 1.0 / scaleFactor, self.transformationSegment.scale.z *= 1.0 / scaleFactor); self.colorIndex = Math.max(0, self.colorIndex - 1); }, '[': () => { self.stack.push(self.transformationSegment.clone()); }, ']': () => { self.transformationSegment = self.stack.pop(); } } }); }, /** * Called when component is attached and when component data changes. * Generally modifies the entity based on the data. */ update: function update(oldData) { // var diffData = diff(data, oldData || {}); // console.log(diffData); // TODO: Check if only angle changed or axiom or productions // let self = this; if (this.data.mergeGeometries === false && this.segmentElementGroupsMap !== undefined) { var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = this.segmentElementGroupsMap.values()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { let segmentElGroup = _step2.value; segmentElGroup.removeObject3D('mesh'); segmentElGroup.innerHTML = ''; } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } if (Object.keys(oldData).length === 0) { this.updateLSystem(); this.updateSegmentMixins(); this.updateTurtleGraphics(); } else { let visualChange = false; if (oldData.axiom && oldData.axiom !== this.data.axiom || oldData.iterations && oldData.iterations !== this.data.iterations || oldData.productions && JSON.stringify(oldData.productions) !== JSON.stringify(this.data.productions)) { this.updateLSystem(); visualChange = true; } if (oldData.segmentMixins !== undefined && JSON.stringify(Array.from(oldData.segmentMixins.entries())) !== JSON.stringify(Array.from(this.data.segmentMixins.entries()))) { this.updateSegmentMixins(); visualChange = true; } if (visualChange || oldData.angle && oldData.angle !== this.data.angle) { this.updateTurtleGraphics(); } else { // console.log('nothing changed in update?'); // this.updateLSystem(); // this.updateSegmentMixins(); } } }, // if this.dynamicSegmentLength===true use this function to set the length // depending on segments geometries bbox calculateSegmentLength: function calculateSegmentLength(mixin, geometry) { if (this.segmentLengthMap.has(mixin)) return this.segmentLengthMap.get(mixin); geometry.computeBoundingBox(); let segmentLength; if (this.data.translateAxis.equals(this.X)) { segmentLength = Math.abs(geometry.boundingBox.min.x - geometry.boundingBox.max.x); } else if (this.data.translateAxis.equals(this.Y)) { segmentLength = Math.abs(geometry.boundingBox.min.y - geometry.boundingBox.max.y); } else if (this.data.translateAxis.equals(this.Z)) { segmentLength = Math.abs(geometry.boundingBox.min.z - geometry.boundingBox.max.z); } this.segmentLengthMap.set(mixin, segmentLength); return segmentLength; }, initWorker: function initWorker() { this.worker = new __WEBPACK_IMPORTED_MODULE_1_worker_loader_inline_fallback_false_worker_js___default.a(); }, pushSegment: function pushSegment(symbol) { let self = this; let currentQuaternion = self.transformationSegment.quaternion; let currentPosition = self.transformationSegment.position; let currentScale = self.transformationSegment.scale; // Cap colorIndex to maximum mixins defined for the symbol. let cappedColorIndex = Math.min(this.colorIndex, this.data.segmentMixins.get(symbol).length - 1); let mixin = this.mixinMap.get(symbol + cappedColorIndex); if (this.data.mergeGeometries === false) { let newSegment = document.createElement('a-entity'); newSegment.setAttribute('mixin', mixin); newSegment.addEventListener('loaded', e => { // Offset child element of object3D, to rotate around end point // IMPORTANT: It may change that A-Frame puts objects into a group let segmentLength = self.segmentLengthMap.get(mixin); newSegment.object3D.children[0].translateOnAxis(self.data.translateAxis, segmentLength * self.segmentLengthFactor / 2); newSegment.object3D.quaternion.copy(currentQuaternion); newSegment.object3D.position.copy(currentPosition); newSegment.object3D.scale.copy(currentScale); }, { once: true }); this.segmentElementGroupsMap.get(symbol + cappedColorIndex).appendChild(newSegment); } else { let segmentObject3D = this.segmentObjects3DMap.get(symbol + cappedColorIndex); let newSegmentObject3D = segmentObject3D.clone(); newSegmentObject3D.matrixAutoUpdate = false; newSegmentObject3D.quaternion.copy(currentQuaternion); newSegmentObject3D.position.copy(currentPosition); newSegmentObject3D.scale.copy(currentScale); newSegmentObject3D.updateMatrix(); this.mergeGroups.get(symbol + cappedColorIndex).geometry.merge(newSegmentObject3D.geometry, newSegmentObject3D.matrix); } let segmentLength = this.segmentLengthMap.get(mixin); this.transformationSegment.translateOnAxis(this.data.translateAxis, segmentLength * this.segmentLengthFactor); }, updateLSystem: function updateLSystem() { let self = this; // post params to worker let params = { axiom: this.data.axiom, productions: this.data.productions, iterations: this.data.iterations }; if (Date.now() - this.worker.startTime > 1000) { // if we got user input, but worker is running for over a second // terminate old worker and start new one. this.worker.terminate(); this.initWorker(); } this.worker.startTime = Date.now(); this.workerPromise = new Promise((resolve, reject) => { this.worker.onmessage = e => { self.LSystem.setAxiom(e.data.result); resolve(); }; }); this.worker.postMessage(params); return this.workerPromise; }, updateSegmentMixins: function updateSegmentMixins() { let self = this; this.el.innerHTML = ''; // Map for remembering the elements holding differnt segment types this.segmentElementGroupsMap = new Map(); this.mixinMap = new Map(); // Construct a map with keys = `symbol + colorIndex` from data.segmentMixins var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = this.data.segmentMixins[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { let _ref5 = _step3.value; var _ref6 = _slicedToArray(_ref5, 2); let symbol = _ref6[0]; let mixinList = _ref6[1]; for (let i = 0; i < mixinList.length; i++) { this.mixinMap.set(symbol + i, mixinList[i]); } } // Map for buffering geometries for use in pushSegments() // when merging geometries ourselves and not by appending a `mixin` attributes, // as done with `mergeGeometry = false`. } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } this.segmentObjects3DMap = new Map(); this.segmentLengthMap = new Map(); this.mergeGroups = new Map(); this.mixinPromises = []; // Collect mixin info by pre-appending segment elements with their mixin // Then use the generated geometry etc. if (this.data.segmentMixins && this.data.segmentMixins.length !== 0) { // Go through every symbols segmentMixins as defined by user var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = this.data.segmentMixins[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { let el = _step4.value; var _el = _slicedToArray(el, 2); let symbol = _el[0], mixinList = _el[1]; // Set final functions for each symbol that has a mixin defined this.LSystem.setFinal(symbol, () => { self.pushSegment.bind(self, symbol)(); }); // And iterate the MixinList to buffer the segments or calculate segment lengths… for (let i = 0; i < mixinList.length; i++) { let mixinColorIndex = i; let mixin = mixinList[mixinColorIndex]; self.mixinPromises.push(new Promise((resolve, reject) => { // Save mixinColorIndex for async promise below. let segmentElGroup = document.createElement('a-entity'); segmentElGroup.setAttribute('id', mixin + '-group-' + mixinColorIndex + Math.floor(Math.random() * 10000)); // TODO: Put it all under this.mergeData segmentElGroup.setAttribute('geometry', 'buffer', false); segmentElGroup.setAttribute('mixin', mixin); segmentElGroup.addEventListener('loaded', function (e) { let segmentObject = segmentElGroup.getObject3D('mesh').clone(); // Make sure the geometry is actually unique // AFrame sets the same geometry for multiple entities. As we modify // the geometry per entity we need to have unique geometry instances. // TODO: hm, maybe try to use instanced geometry and offset on object? segmentElGroup.getObject3D('mesh').geometry.dispose(); segmentObject.geometry = segmentObject.geometry.clone(); let segmentLength = self.calculateSegmentLength(mixin, segmentObject.geometry); // Do some additional stuff like buffering 3D objects / geometry // if we want to merge geometries. if (self.data.mergeGeometries === true) { // Offset geometry by half segmentLength to get the rotation point right. let translation = self.data.translateAxis.clone().multiplyScalar(segmentLength * self.segmentLengthFactor / 2); // IMPORTANT!!! // TODO: Try to use pivot object instead of translating geometry // this may help in reusing geometry and not needing to clone it (see above)? // see: https://github.com/mrdoob/three.js/issues/1364 //and // see: http://stackoverflow.com/questions/28848863/threejs-how-to-rotate-around-objects-own-center-instead-of-world-center segmentObject.geometry.translate(translation.x, translation.y, translation.z); self.segmentObjects3DMap.set(symbol + mixinColorIndex, segmentObject); } segmentElGroup.removeObject3D('mesh'); resolve(); }, { once: true }); if (this.segmentElementGroupsMap.has(symbol + mixinColorIndex)) { let previousElGroup = this.segmentElementGroupsMap.get(symbol + mixinColorIndex); this.segmentElementGroupsMap.delete(symbol + mixinColorIndex); this.el.removeChild(previousElGroup); } this.segmentElementGroupsMap.set(symbol + mixinColorIndex, segmentElGroup); this.el.appendChild(segmentElGroup); })); } } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } } }, updateTurtleGraphics: async function updateTurtleGraphics() { await Promise.all([...this.mixinPromises, this.workerPromise]); // The main segment used for saving transformations (rotation, translation, scale(?)) this.transformationSegment.copy(this.transformationSegmentTemplate); // set merge groups if (this.data.mergeGeometries === true) { var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = this.segmentObjects3DMap[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { let _ref7 = _step5.value; var _ref8 = _slicedToArray(_ref7, 2); let id = _ref8[0]; let segmentObject = _ref8[1]; this.mergeGroups.set(id, new THREE.Mesh(new THREE.Geometry(), segmentObject.material)); } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } } // We push copies of this.transformationSegment on branch symbols inside this array. this.stack = []; let angle = this.data.angle; // Set quaternions based on angle slider this.xPosRotation.setFromAxisAngle(this.X, Math.PI / 180 * angle); this.xNegRotation.setFromAxisAngle(this.X, Math.PI / 180 * -angle); this.yPosRotation.setFromAxisAngle(this.Y, Math.PI / 180 * angle); this.yNegRotation.setFromAxisAngle(this.Y, Math.PI / 180 * -angle); this.yReverseRotation.setFromAxisAngle(this.Y, Math.PI / 180 * 180); this.zPosRotation.setFromAxisAngle(this.Z, Math.PI / 180 * angle); this.zNegRotation.setFromAxisAngle(this.Z, Math.PI / 180 * -angle); // // this.geometry = new THREE.CylinderGeometry(this.lineWidth, this.lineWidth, self.data.lineLength, 3); // this.geometry.rotateZ((Math.PI / 180) * 90); // this.geometry.translate( -(this.data.segmentLength/2), 0, 0 ); // for (let face of this.geometry.faces) { // face.color.setHex(this.colors[colorIndex]); // } // this.geometry.colorsNeedUpdate = true; this.LSystem.final(); // finally set the merged meshes to be visible. if (this.data.mergeGeometries === true) { var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = this.segmentElementGroupsMap[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { let tuple = _step6.value; var _tuple = _slicedToArray(tuple, 2); let symbolWithColorIndex = _tuple[0], elGroup = _tuple[1]; let mergeGroup = this.mergeGroups.get(symbolWithColorIndex); // Remove unused element groups inside our element if (mergeGroup.geometry.vertices.length === 0) { this.el.removeChild(elGroup); } else { elGroup.setObject3D('mesh', this.mergeGroups.get(symbolWithColorIndex)); elGroup.setAttribute('mixin', this.mixinMap.get(symbolWithColorIndex)); } } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } } }, /** * Called when a component is removed (e.g., via removeAttribute). * Generally undoes all modifications to the entity. */ remove: function remove() {}, /** * Called on each scene tick. */ tick: function tick(t) { // console.log(this.parentEl === undefined); // console.log('\nTICK\n', t); }, /** * Called when entity pauses. * Use to stop or remove any dynamic or background behavior such as events. */ pause: function pause() {}, /** * Called when entity resumes. * Use to continue or add any dynamic or background behavior such as events. */ play: function play() {} }); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; // 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 = void 0; // 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) { if (typeof string !== 'string' && string instanceof String === false) return string; var transformed = []; for (var _iterator = string, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref; if (_isArray) { if (_i >= _iterator.length) break; _ref = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref = _i.value; } var symbol = _ref; transformed.push({ symbol }); }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; } function LSystem(_ref) { var _ref$axiom = _ref.axiom, axiom = _ref$axiom === undefined ? '' : _ref$axiom, productions = _ref.productions, finals = _ref.finals, _ref$branchSymbols = _ref.branchSymbols, branchSymbols = _ref$branchSymbols === undefined ? '' : _ref$branchSymbols, _ref$ignoredSymbols = _ref.ignoredSymbols, ignoredSymbols = _ref$ignoredSymbols === undefined ? '' : _ref$ignoredSymbols, _ref$allowClassicSynt = _ref.allowClassicSyntax, allowClassicSyntax = _ref$allowClassicSynt === undefined ? true : _ref$allowClassicSynt, _ref$classicParametri = _ref.classicParametricSyntax, classicParametricSyntax = _ref$classicParametri === undefined ? false : _ref$classicParametri, _ref$forceObjects = _ref.forceObjects, forceObjects = _ref$forceObjects === undefined ? false : _ref$forceObjects, _ref$debug = _ref.debug, debug = _ref$debug === undefined ? false : _ref$debug; // TODO: forceObject to be more intelligent based on other productions?? this.setAxiom = function (axiom) { this.axiom = this.forceObjects ? stringToObjects(axiom) : axiom; }; this.getRaw = function () { return this.axiom; }; // if using objects in axioms, as used in parametric L-Systems this.getString = function () { var onlySymbols = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (typeof this.axiom === 'string') return this.axiom; if (onlySymbols === true) { return this.axiom.reduce((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(this.axiom); } }; this.getStringResult = this.getString; this.setProduction = function (from, to) { var allowAppendingMultiSuccessors = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 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 (this.allowClassicSyntax === true) { newProduction = transformClassicCSProduction(newProduction, this.ignoredSymbols); } newProduction = normalizeProduction(newProduction, this.forceObjects); // check wether production is stochastic newProduction[1].isStochastic = newProduction[1].successors !== undefined && newProduction[1].successors.every(successor => successor.weight !== undefined); if (newProduction[1].isStochastic) { // calculate weight sum newProduction[1].weightSum = 0; for (var _iterator = newProduction[1].successors, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref2; if (_isArray) { if (_i >= _iterator.length) break; _ref2 = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref2 = _i.value; } var s = _ref2; newProduction[1].weightSum += s.weight; } } var symbol = newProduction[0]; if (allowAppendingMultiSuccessors === true && this.productions.has(symbol)) { var existingProduction = this.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]); this.productions.set(symbol, existingProduction); } else { this.productions.set(symbol, newProduction[1]); } }; // set multiple productions from name:value Object // TODO: ALLOW TUPLE/ARRAY this.setProductions = function (newProductions) { if (newProductions === undefined) throw new Error('no production specified.'); this.clearProductions(); for (var _iterator2 = Object.entries(newProductions), _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { var _ref4; if (_isArray2) { if (_i2 >= _iterator2.length) break; _ref4 = _iterator2[_i2++]; } else { _i2 = _iterator2.next(); if (_i2.done) break; _ref4 = _i2.value; } var _ref3 = _ref4; var from = _ref3[0]; var to = _ref3[1]; this.setProduction(from, to, true); } }; this.clearProductions = function () { this.productions = new Map(); }; this.setFinal = function (symbol, final) { var newFinal = [symbol, final]; if (newFinal === undefined) { throw new Error('no final specified.'); } this.finals.set(newFinal[0], newFinal[1]); }; // set multiple finals from name:value Object this.setFinals = function (newFinals) { if (newFinals === undefined) throw new Error('no finals specified.'); this.finals = new Map(); for (var symbol in newFinals) { if (newFinals.hasOwnProperty(symbol)) { this.setFinal(symbol, newFinals[symbol]); } } }; //var hasWeight = el => el.weight !== undefined; this.getProductionResult = function (p, index, part, params) { var recursive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 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, currentAxiom: this.axiom, part, params }) === false) { precheck = false; } else if (contextSensitive) { if (p.leftCtx !== undefined && p.rightCtx !== undefined) { precheck = this.match({ direction: 'left', match: p.leftCtx, index: index, branchSymbols: '[]' }).result && this.match({ direction: 'right', match: p.rightCtx, index: index, branchSymbols: '[]', ignoredSymbols: ignoredSymbols }).result; } else if (p.leftCtx !== undefined) { precheck = this.match({ direction: 'left', match: p.leftCtx, index: index, branchSymbols: '[]' }).result; } else if (p.rightCtx !== undefined) { precheck = this.match({ direction: 'right', match: p.rightCtx, index: index, branchSymbols: '[]' }).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 // Tread 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 _iterator3 = p.successors, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { var _ref5; if (_isArray3) { if (_i3 >= _iterator3.length) break; _ref5 = _iterator3[_i3++]; } else { _i3 = _iterator3.next(); if (_i3.done) break; _ref5 = _i3.value; } var _p = _ref5; 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 var _result = this.getProductionResult(_p, index, part, params, true); // console.log(part, p.successors); // console.log(result); // console.log("\n"); 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, currentAxiom: this.axiom, part, params }); } else { result = p.successor; } if (!result) { // Allow undefined or false results for recursive calls of this func return recursive ? result : part; } return result; }; this.applyProductions = function () { // a axiom can be a string or an array of objects that contain the key/value 'symbol' var newAxiom = typeof this.axiom === 'string' ? '' : []; var index = 0; // iterate all symbols/characters of the axiom and lookup according productions for (var _iterator4 = this.axiom, _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { var _ref6; if (_isArray4) { if (_i4 >= _iterator4.length) break; _ref6 = _iterator4[_i4++]; } else { _i4 = _iterator4.next(); if (_i4.done) break; _ref6 = _i4.value; } var part = _ref6; // 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 (this.productions.has(symbol)) { var p = this.productions.get(symbol); result = this.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. Array.prototype.push.apply(newAxiom, result); } else { newAxiom.push(result); } index++; } // finally set new axiom and also return it for convenience. this.axiom = newAxiom; return newAxiom; }; this.iterate = function () { var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; this.iterations = n; var lastIteration = void 0; for (var iteration = 0; iteration < n; iteration++) { lastIteration = this.applyProductions(); } return lastIteration; }; this.final = function (externalArg) { var index = 0; for (var _iterator5 = this.axiom, _isArray5 = Array.isArray(_iterator5), _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) { var _ref7; if (_isArray5) { if (_i5 >= _iterator5.length) break; _ref7 = _iterator5[_i5++]; } else { _i5 = _iterator5.next(); if (_i5.done) break; _ref7 = _i5.value; } var part = _ref7; // 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 (this.finals.has(symbol)) { var finalFunction = this.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, part }, externalArg); } else { // symbol has no final function } index++; } }; /* 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 */ this.match = function (_ref8) { var axiom_ = _ref8.axiom_, match = _ref8.match, ignoredSymbols = _ref8.ignoredSymbols, branchSymbols = _ref8.branchSymbols, index = _ref8.index, direction = _ref8.direction; var branchCount = 0; var explicitBranchCount = 0; axiom_ = axiom_ || this.axiom; if (branchSymbols === undefined) branchSymbols = this.branchSymbols !== undefined ? this.branchSymbols : []; if (ignoredSymbols === undefined) ignoredSymbols = this.ignoredSymbols !== undefined ? this.ignoredSymbols : []; var returnMatchIndices = []; var branchStart = void 0, branchEnd = void 0, axiomIndex = void 0, loopIndexChange = void 0, matchIndex = void 0, matchIndexChange = void 0, matchIndexOverflow = void 0; // 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 }; }; this.ignoredSymbols = ignoredSymbols; this.debug = debug; this.branchSymbols = branchSymbols; this.allowClassicSyntax = allowClassicSyntax; this.classicParametricSyntax = classicParametricSyntax; this.forceObjects = forceObjects; this.setAxiom(axiom); this.clearProductions(); if (productions) this.setProductions(productions); if (finals) this.setFinals(finals); return this; } // Set classic syntax helpers to library scope to be used outside of library context // for users eg. LSystem.transformClassicStochasticProductions = transformClassicStochasticProductions; LSystem.transformClassicCSProduction = transformClassicCSProduction; LSystem.transformClassicParametricAxiom = transformClassicParametricAxiom; LSystem.testClassicParametricSyntax = testClassicParametricSyntax; /* harmony default export */ __webpack_exports__["a"] = (LSystem); /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { module.exports = function() { return __webpack_require__(3)("/******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[mod