From 2769d6e74a515c667837211e0a109e7489826291 Mon Sep 17 00:00:00 2001 From: Amphiluke Date: Wed, 3 Jan 2024 19:59:52 +0700 Subject: [PATCH] =?UTF-8?q?Add=20support=20for=20the=20=E2=80=9Cturn=20aro?= =?UTF-8?q?und=E2=80=9D=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +++++++++++++++ dist/lindsvg.cjs | 25 ++++++++++++++++++------- dist/lindsvg.esm.js | 25 ++++++++++++++++++------- dist/lindsvg.esm.min.js | 6 +++--- dist/lindsvg.js | 25 ++++++++++++++++++------- dist/lindsvg.min.js | 6 +++--- package-lock.json | 4 ++-- package.json | 2 +- src/generator.mjs | 5 +++-- src/svg.mjs | 6 ++++++ src/turtle.mjs | 4 ++++ src/validator.mjs | 6 +++--- test/install-test/package-lock.json | 8 ++++---- test/install-test/package.json | 2 +- 14 files changed, 99 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 719b1b1..569bb63 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,21 @@ If you rather prefer using ES modules in a browser, just choose the “esm” bu ``` +## Supported commands + +The following turtle commands are currently supported by lindsvg: + +| Command | Description | +| ------------------- | ----------------------------------------------------- | +| `F` | Move forward one step with drawing a line | +| `B` | Move forward one step without drawing a line | +| `+` | Turn left by turning angle (theta) | +| `-` | Turn right by turning angle (theta) | +| `\|` | Reverse direction (turn by 180 degrees) | +| `[` | Push current state of the turtle onto the stack | +| `]` | Pop a state from the stack and apply it to the turtle | +| `A`,`C`–`E`,`G`–`Z` | Auxiliary user-defined rules | + ## API & examples The module exports two pairs of methods. diff --git a/dist/lindsvg.cjs b/dist/lindsvg.cjs index 493a5cf..017af5d 100644 --- a/dist/lindsvg.cjs +++ b/dist/lindsvg.cjs @@ -1,13 +1,13 @@ /*! -lindsvg v1.3.3 +lindsvg v1.4.0 https://amphiluke.github.io/lindsvg/ -(c) 2023 Amphiluke +(c) 2024 Amphiluke */ 'use strict'; let messages = { - AXIOM: "Axiom may only contain the following characters: A..Z,+,-,[,]", - RULE: "Production rules may only contain the following characters: A..Z,+,-,[,]", + AXIOM: "Axiom may only contain the following characters: A..Z,+,-,|,[,]", + RULE: "Production rules may only contain the following characters: A..Z,+,-,|,[,]", LETTER: "Allowed alphabet letters are: A..Z", ALPHA: "The “alpha” parameter must be a finite number", THETA: "The “theta” parameter must be a finite number", @@ -21,7 +21,7 @@ function checkLetter(letter, msg = messages.LETTER) { return letterRE.test(letter) || msg; } -let ruleRE = /^[A-Z+\-[\]]*$/; +let ruleRE = /^[A-Z+\-[\]|]*$/; function checkRule(rule, msg = messages.RULE) { return ruleRE.test(rule) || msg; } @@ -117,6 +117,7 @@ let ctrlRules = { B: "", "+": "+", "-": "-", + "|": "|", "[": "[", "]": "]", }; @@ -136,7 +137,7 @@ let defaults = { */ function cleanCodeword(codeword) { // Remove auxiliary drawing-indifferent letters - let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, ""); + let cleanCodeword = codeword.replace(/[^FB[\]+-|]/g, ""); do { codeword = cleanCodeword; // Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!) @@ -169,7 +170,7 @@ function generateCodeword(lsParams) { * @return {String[]} */ function tokenizeCodeword(codeword) { - return codeword.match(/([FB[\]+-])\1*/g); // tokenize + return codeword.match(/([FB[\]+-|])\1*/g); // tokenize } class Turtle { @@ -195,6 +196,10 @@ class Turtle { this.alpha += factor * this.theta; } + reverse(repeatCount = 1) { + this.alpha += (repeatCount % 2) * Math.PI; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -263,6 +268,9 @@ function getPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -312,6 +320,9 @@ function getMultiPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index 9b86846..6716570 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,11 +1,11 @@ /*! -lindsvg v1.3.3 +lindsvg v1.4.0 https://amphiluke.github.io/lindsvg/ -(c) 2023 Amphiluke +(c) 2024 Amphiluke */ let messages = { - AXIOM: "Axiom may only contain the following characters: A..Z,+,-,[,]", - RULE: "Production rules may only contain the following characters: A..Z,+,-,[,]", + AXIOM: "Axiom may only contain the following characters: A..Z,+,-,|,[,]", + RULE: "Production rules may only contain the following characters: A..Z,+,-,|,[,]", LETTER: "Allowed alphabet letters are: A..Z", ALPHA: "The “alpha” parameter must be a finite number", THETA: "The “theta” parameter must be a finite number", @@ -19,7 +19,7 @@ function checkLetter(letter, msg = messages.LETTER) { return letterRE.test(letter) || msg; } -let ruleRE = /^[A-Z+\-[\]]*$/; +let ruleRE = /^[A-Z+\-[\]|]*$/; function checkRule(rule, msg = messages.RULE) { return ruleRE.test(rule) || msg; } @@ -115,6 +115,7 @@ let ctrlRules = { B: "", "+": "+", "-": "-", + "|": "|", "[": "[", "]": "]", }; @@ -134,7 +135,7 @@ let defaults = { */ function cleanCodeword(codeword) { // Remove auxiliary drawing-indifferent letters - let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, ""); + let cleanCodeword = codeword.replace(/[^FB[\]+-|]/g, ""); do { codeword = cleanCodeword; // Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!) @@ -167,7 +168,7 @@ function generateCodeword(lsParams) { * @return {String[]} */ function tokenizeCodeword(codeword) { - return codeword.match(/([FB[\]+-])\1*/g); // tokenize + return codeword.match(/([FB[\]+-|])\1*/g); // tokenize } class Turtle { @@ -193,6 +194,10 @@ class Turtle { this.alpha += factor * this.theta; } + reverse(repeatCount = 1) { + this.alpha += (repeatCount % 2) * Math.PI; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -261,6 +266,9 @@ function getPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -310,6 +318,9 @@ function getMultiPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index 4057a53..6cfaa23 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.3.3 +lindsvg v1.4.0 https://amphiluke.github.io/lindsvg/ -(c) 2023 Amphiluke +(c) 2024 Amphiluke */ -let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function i(e,i=t.RULE){return a.test(e)||i}function r(a,r,n){let h=Object.create(null);return Object.entries(a).forEach((([a,s])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===c&&(c=i(s,n)),!0!==c&&(h[a]=c)})),!Object.keys(h).length||h}function n(e){let a=Object.create(null);return Object.entries(e).forEach((([e,n])=>{let h=!0;switch(e){case"axiom":h=i(n,t.AXIOM);break;case"rules":h=r(n);break;case"alpha":case"theta":h=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":h=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":h=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==h&&(a[e]=h)})),!Object.keys(a).length||a}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let s={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function o(t){let e=n(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...s,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(r[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function l(t){return t.match(/([FB[\]+-])\1*/g)}class u{constructor({x:t,y:e,step:a,alpha:i,theta:r}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=a,this.alpha=-i,this.theta=r}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)}rotate(t){this.alpha+=t*this.theta}pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())}getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}}function p(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function f(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a;return m(t.reduce(((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${p(e.x,e.y)}`,a="M"}return t}),"M"+p(e.x,e.y)))}(l(e),a);return{pathData:i,...a.getDrawingRect()}}function x(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,r)=>{let n=t[i]||"",h=r.length;switch(r[0]){case"F":e.translate(h),n+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(h),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(h);break;case"-":e.rotate(-h);break;case"[":i+=h,e.pushStack(h),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M";break;case"]":i-=h,e.popStack(h),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M"}return t[i]=n,t}),["M"+p(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(l(e),a);return{multiPathData:i,...a.getDrawingRect()}}function d(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function b({viewBox:t,width:e,height:a,content:i}){return`${i}`}function g(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i||"n/a"===i.toLowerCase()?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}function w(t,e){let{pathData:a,minX:i,minY:r,width:n,height:h}=f(t),{padding:s,width:c,height:o,pathAttributes:l}=d(e,n,h);return b({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:``})}function y(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:h}=x(t),{padding:s,width:c,height:o,pathAttributes:l}=d(e,n,h);return b({viewBox:[i-s,r-s,n+2*s,h+2*s],width:c,height:o,content:a.reduce(((t,e,a)=>`${t}`),"")})}export{y as getMultiPathSVGCode,x as getMultiPathSVGData,w as getSVGCode,f as getSVGData}; +let t={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,|,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,|,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},e=/^[A-Z]$/;let a=/^[A-Z+\-[\]|]*$/;function i(e,i=t.RULE){return a.test(e)||i}function r(a,r,n){let s=Object.create(null);return Object.entries(a).forEach((([a,h])=>{let c=function(a,i=t.LETTER){return e.test(a)||i}(a,r);!0===c&&(c=i(h,n)),!0!==c&&(s[a]=c)})),!Object.keys(s).length||s}function n(e){let a=Object.create(null);return Object.entries(e).forEach((([e,n])=>{let s=!0;switch(e){case"axiom":s=i(n,t.AXIOM);break;case"rules":s=r(n);break;case"alpha":case"theta":s=function(e,a=t.NUMBER){return Number.isFinite(e)||a}(n,t[e.toUpperCase()]);break;case"step":s=function(e,a=t.STEP){return Number.isFinite(e)&&e>0||a}(n);break;case"iterations":s=function(e,a=t.COUNT){return Number.isInteger(e)&&e>0||a}(n)}!0!==s&&(a[e]=s)})),!Object.keys(a).length||a}class s extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(s.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let h={F:"",B:"","+":"+","-":"-","|":"|","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function o(t){let e=n(t);if(!0!==e)throw new s(e);let{axiom:a,iterations:i}={...c,...t},r={...h,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(r[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-|]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function l(t){return t.match(/([FB[\]+-|])\1*/g)}class u{constructor({x:t,y:e,step:a,alpha:i,theta:r}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=a,this.alpha=-i,this.theta=r}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)}rotate(t){this.alpha+=t*this.theta}reverse(t=1){this.alpha+=t%2*Math.PI}pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())}getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}}function p(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function f(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a;return m(t.reduce(((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"|":e.reverse(r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${p(e.x,e.y)}`,a="M"}return t}),"M"+p(e.x,e.y)))}(l(e),a);return{pathData:i,...a.getDrawingRect()}}function x(t){let e=o(t),a=new u({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,r)=>{let n=t[i]||"",s=r.length;switch(r[0]){case"F":e.translate(s),n+=("L"===a?" ":"L")+p(e.x,e.y),a="L";break;case"B":e.translate(s),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+p(e.x,e.y),a="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"|":e.reverse(s);break;case"[":i+=s,e.pushStack(s),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M";break;case"]":i-=s,e.popStack(s),n=`${t[i]||""}M${p(e.x,e.y)}`,a="M"}return t[i]=n,t}),["M"+p(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(l(e),a);return{multiPathData:i,...a.getDrawingRect()}}function b(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function d({viewBox:t,width:e,height:a,content:i}){return`${i}`}function g(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i||"n/a"===i.toLowerCase()?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}function w(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=f(t),{padding:h,width:c,height:o,pathAttributes:l}=b(e,n,s);return d({viewBox:[i-h,r-h,n+2*h,s+2*h],width:c,height:o,content:``})}function M(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:s}=x(t),{padding:h,width:c,height:o,pathAttributes:l}=b(e,n,s);return d({viewBox:[i-h,r-h,n+2*h,s+2*h],width:c,height:o,content:a.reduce(((t,e,a)=>`${t}`),"")})}export{M as getMultiPathSVGCode,x as getMultiPathSVGData,w as getSVGCode,f as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 4241471..4d58265 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,7 +1,7 @@ /*! -lindsvg v1.3.3 +lindsvg v1.4.0 https://amphiluke.github.io/lindsvg/ -(c) 2023 Amphiluke +(c) 2024 Amphiluke */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : @@ -10,8 +10,8 @@ https://amphiluke.github.io/lindsvg/ })(this, (function (exports) { 'use strict'; let messages = { - AXIOM: "Axiom may only contain the following characters: A..Z,+,-,[,]", - RULE: "Production rules may only contain the following characters: A..Z,+,-,[,]", + AXIOM: "Axiom may only contain the following characters: A..Z,+,-,|,[,]", + RULE: "Production rules may only contain the following characters: A..Z,+,-,|,[,]", LETTER: "Allowed alphabet letters are: A..Z", ALPHA: "The “alpha” parameter must be a finite number", THETA: "The “theta” parameter must be a finite number", @@ -25,7 +25,7 @@ https://amphiluke.github.io/lindsvg/ return letterRE.test(letter) || msg; } - let ruleRE = /^[A-Z+\-[\]]*$/; + let ruleRE = /^[A-Z+\-[\]|]*$/; function checkRule(rule, msg = messages.RULE) { return ruleRE.test(rule) || msg; } @@ -121,6 +121,7 @@ https://amphiluke.github.io/lindsvg/ B: "", "+": "+", "-": "-", + "|": "|", "[": "[", "]": "]", }; @@ -140,7 +141,7 @@ https://amphiluke.github.io/lindsvg/ */ function cleanCodeword(codeword) { // Remove auxiliary drawing-indifferent letters - let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, ""); + let cleanCodeword = codeword.replace(/[^FB[\]+-|]/g, ""); do { codeword = cleanCodeword; // Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!) @@ -173,7 +174,7 @@ https://amphiluke.github.io/lindsvg/ * @return {String[]} */ function tokenizeCodeword(codeword) { - return codeword.match(/([FB[\]+-])\1*/g); // tokenize + return codeword.match(/([FB[\]+-|])\1*/g); // tokenize } class Turtle { @@ -199,6 +200,10 @@ https://amphiluke.github.io/lindsvg/ this.alpha += factor * this.theta; } + reverse(repeatCount = 1) { + this.alpha += (repeatCount % 2) * Math.PI; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -267,6 +272,9 @@ https://amphiluke.github.io/lindsvg/ case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -316,6 +324,9 @@ https://amphiluke.github.io/lindsvg/ case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index 88320b8..3907623 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.3.3 +lindsvg v1.4.0 https://amphiluke.github.io/lindsvg/ -(c) 2023 Amphiluke +(c) 2024 Amphiluke */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},i=/^[A-Z]$/;let a=/^[A-Z+\-[\]]*$/;function n(t,i=e.RULE){return a.test(t)||i}function r(t,a,r){let s=Object.create(null);return Object.entries(t).forEach((([t,h])=>{let o=function(t,a=e.LETTER){return i.test(t)||a}(t,a);!0===o&&(o=n(h,r)),!0!==o&&(s[t]=o)})),!Object.keys(s).length||s}function s(t){let i=Object.create(null);return Object.entries(t).forEach((([t,a])=>{let s=!0;switch(t){case"axiom":s=n(a,e.AXIOM);break;case"rules":s=r(a);break;case"alpha":case"theta":s=function(t,i=e.NUMBER){return Number.isFinite(t)||i}(a,e[t.toUpperCase()]);break;case"step":s=function(t,i=e.STEP){return Number.isFinite(t)&&t>0||i}(a);break;case"iterations":s=function(t,i=e.COUNT){return Number.isInteger(t)&&t>0||i}(a)}!0!==s&&(i[t]=s)})),!Object.keys(i).length||i}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let o={F:"",B:"","+":"+","-":"-","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function l(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:i,iterations:a}={...c,...t},n={...o,...t.rules};for(;a>0;a--)i=[...i].reduce(((t,e)=>t+(n[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(i)}function u(t){return t.match(/([FB[\]+-])\1*/g)}class p{constructor({x:t,y:e,step:i,alpha:a,theta:n}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=i,this.alpha=-a,this.theta=n}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)}rotate(t){this.alpha+=t*this.theta}pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())}getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),i=Math.ceil(this.maxX),a=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:i,maxY:a,width:i-t,height:a-e}}}function f(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function d(t){let e=l(t),i=new p({x:0,y:0,...t}),a=function(t,e){let i;return m(t.reduce(((t,a)=>{let n=a.length;switch(a[0]){case"F":e.translate(n),t+=("L"===i?" ":"L")+f(e.x,e.y),i="L";break;case"B":e.translate(n),"M"===i&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+f(e.x,e.y),i="M";break;case"+":e.rotate(n);break;case"-":e.rotate(-n);break;case"[":e.pushStack(n);break;case"]":e.popStack(n),t+=`M${f(e.x,e.y)}`,i="M"}return t}),"M"+f(e.x,e.y)))}(u(e),i);return{pathData:a,...i.getDrawingRect()}}function g(t){let e=l(t),i=new p({x:0,y:0,...t}),a=function(t,e){let i,a=0;return t.reduce(((t,n)=>{let r=t[a]||"",s=n.length;switch(n[0]){case"F":e.translate(s),r+=("L"===i?" ":"L")+f(e.x,e.y),i="L";break;case"B":e.translate(s),"M"===i&&(r=r.slice(0,r.lastIndexOf("M"))),r+="M"+f(e.x,e.y),i="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"[":a+=s,e.pushStack(s),r=`${t[a]||""}M${f(e.x,e.y)}`,i="M";break;case"]":a-=s,e.popStack(s),r=`${t[a]||""}M${f(e.x,e.y)}`,i="M"}return t[a]=r,t}),["M"+f(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(u(e),i);return{multiPathData:a,...i.getDrawingRect()}}function x(t,e,i){return{width:t.width||e,height:t.height||i,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function b({viewBox:t,width:e,height:i,content:a}){return`${a}`}function y(t,e){return Object.entries(t).reduce(((t,[i,a])=>(Array.isArray(a)&&(a=a[Math.min(e,a.length-1)]),void 0===a||"n/a"===a.toLowerCase()?t:`${t} ${i}="${a=a.replace(/"/g,""")}"`)),"")}t.getMultiPathSVGCode=function(t,e){let{multiPathData:i,minX:a,minY:n,width:r,height:s}=g(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,r,s);return b({viewBox:[a-h,n-h,r+2*h,s+2*h],width:o,height:c,content:i.reduce(((t,e,i)=>`${t}`),"")})},t.getMultiPathSVGData=g,t.getSVGCode=function(t,e){let{pathData:i,minX:a,minY:n,width:r,height:s}=d(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,r,s);return b({viewBox:[a-h,n-h,r+2*h,s+2*h],width:o,height:c,content:``})},t.getSVGData=d})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).lindsvg={})}(this,(function(t){"use strict";let e={AXIOM:"Axiom may only contain the following characters: A..Z,+,-,|,[,]",RULE:"Production rules may only contain the following characters: A..Z,+,-,|,[,]",LETTER:"Allowed alphabet letters are: A..Z",ALPHA:"The “alpha” parameter must be a finite number",THETA:"The “theta” parameter must be a finite number",STEP:"The “step” parameter must be a positive finite number",COUNT:"The number of iterations must be integer and finite",NUMBER:"A valid finite number expected"},a=/^[A-Z]$/;let i=/^[A-Z+\-[\]|]*$/;function r(t,a=e.RULE){return i.test(t)||a}function n(t,i,n){let s=Object.create(null);return Object.entries(t).forEach((([t,h])=>{let o=function(t,i=e.LETTER){return a.test(t)||i}(t,i);!0===o&&(o=r(h,n)),!0!==o&&(s[t]=o)})),!Object.keys(s).length||s}function s(t){let a=Object.create(null);return Object.entries(t).forEach((([t,i])=>{let s=!0;switch(t){case"axiom":s=r(i,e.AXIOM);break;case"rules":s=n(i);break;case"alpha":case"theta":s=function(t,a=e.NUMBER){return Number.isFinite(t)||a}(i,e[t.toUpperCase()]);break;case"step":s=function(t,a=e.STEP){return Number.isFinite(t)&&t>0||a}(i);break;case"iterations":s=function(t,a=e.COUNT){return Number.isInteger(t)&&t>0||a}(i)}!0!==s&&(a[t]=s)})),!Object.keys(a).length||a}class h extends Error{constructor(t){let e=JSON.stringify(t,null,2);super(e),Object.defineProperty(this,"lsErrors",{value:JSON.parse(e)})}toJSON(){return JSON.parse(JSON.stringify(this.lsErrors))}}Object.defineProperty(h.prototype,"name",{configurable:!0,enumerable:!1,writable:!0,value:"LSError"});let o={F:"",B:"","+":"+","-":"-","|":"|","[":"[","]":"]"},c={alpha:0,theta:0,step:10,iterations:3};function l(t){let e=s(t);if(!0!==e)throw new h(e);let{axiom:a,iterations:i}={...c,...t},r={...o,...t.rules};for(;i>0;i--)a=[...a].reduce(((t,e)=>t+(r[e]||"")),"");return function(t){let e=t.replace(/[^FB[\]+-|]/g,"");do{t=e,e=e.replace(/\[[^F[\]]*]/g,"")}while(e!==t);return e}(a)}function u(t){return t.match(/([FB[\]+-|])\1*/g)}class p{constructor({x:t,y:e,step:a,alpha:i,theta:r}){this.stack=[],this.x=this.minX=this.maxX=t,this.y=this.minY=this.maxY=e,this.step=a,this.alpha=-i,this.theta=r}translate(t=1){this.x+=t*this.step*Math.cos(this.alpha),this.y+=t*this.step*Math.sin(this.alpha),this.minX=Math.min(this.minX,this.x),this.maxX=Math.max(this.maxX,this.x),this.minY=Math.min(this.minY,this.y),this.maxY=Math.max(this.maxY,this.y)}rotate(t){this.alpha+=t*this.theta}reverse(t=1){this.alpha+=t%2*Math.PI}pushStack(t=1){for(;t>0;t--)this.stack.push({x:this.x,y:this.y,alpha:this.alpha})}popStack(t){for(;t>0;t--)({x:this.x,y:this.y,alpha:this.alpha}=this.stack.pop())}getDrawingRect(){let t=Math.floor(this.minX),e=Math.floor(this.minY),a=Math.ceil(this.maxX),i=Math.ceil(this.maxY);return{minX:t,minY:e,maxX:a,maxY:i,width:a-t,height:i-e}}}function f(t,e){return`${+t.toFixed(4)} ${+e.toFixed(4)}`}function m(t){return t.replace(/(?:M-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?)+(?=M|$)/g,"")}function d(t){let e=l(t),a=new p({x:0,y:0,...t}),i=function(t,e){let a;return m(t.reduce(((t,i)=>{let r=i.length;switch(i[0]){case"F":e.translate(r),t+=("L"===a?" ":"L")+f(e.x,e.y),a="L";break;case"B":e.translate(r),"M"===a&&(t=t.slice(0,t.lastIndexOf("M"))),t+="M"+f(e.x,e.y),a="M";break;case"+":e.rotate(r);break;case"-":e.rotate(-r);break;case"|":e.reverse(r);break;case"[":e.pushStack(r);break;case"]":e.popStack(r),t+=`M${f(e.x,e.y)}`,a="M"}return t}),"M"+f(e.x,e.y)))}(u(e),a);return{pathData:i,...a.getDrawingRect()}}function g(t){let e=l(t),a=new p({x:0,y:0,...t}),i=function(t,e){let a,i=0;return t.reduce(((t,r)=>{let n=t[i]||"",s=r.length;switch(r[0]){case"F":e.translate(s),n+=("L"===a?" ":"L")+f(e.x,e.y),a="L";break;case"B":e.translate(s),"M"===a&&(n=n.slice(0,n.lastIndexOf("M"))),n+="M"+f(e.x,e.y),a="M";break;case"+":e.rotate(s);break;case"-":e.rotate(-s);break;case"|":e.reverse(s);break;case"[":i+=s,e.pushStack(s),n=`${t[i]||""}M${f(e.x,e.y)}`,a="M";break;case"]":i-=s,e.popStack(s),n=`${t[i]||""}M${f(e.x,e.y)}`,a="M"}return t[i]=n,t}),["M"+f(e.x,e.y)]).filter((t=>t.includes("L"))).map(m)}(u(e),a);return{multiPathData:i,...a.getDrawingRect()}}function x(t,e,a){return{width:t.width||e,height:t.height||a,padding:t.padding||0,pathAttributes:{fill:t.fill||"none",stroke:t.stroke||"#000",...t.pathAttributes}}}function b({viewBox:t,width:e,height:a,content:i}){return`${i}`}function y(t,e){return Object.entries(t).reduce(((t,[a,i])=>(Array.isArray(i)&&(i=i[Math.min(e,i.length-1)]),void 0===i||"n/a"===i.toLowerCase()?t:`${t} ${a}="${i=i.replace(/"/g,""")}"`)),"")}t.getMultiPathSVGCode=function(t,e){let{multiPathData:a,minX:i,minY:r,width:n,height:s}=g(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,n,s);return b({viewBox:[i-h,r-h,n+2*h,s+2*h],width:o,height:c,content:a.reduce(((t,e,a)=>`${t}`),"")})},t.getMultiPathSVGData=g,t.getSVGCode=function(t,e){let{pathData:a,minX:i,minY:r,width:n,height:s}=d(t),{padding:h,width:o,height:c,pathAttributes:l}=x(e,n,s);return b({viewBox:[i-h,r-h,n+2*h,s+2*h],width:o,height:c,content:``})},t.getSVGData=d})); diff --git a/package-lock.json b/package-lock.json index d4cf507..8367301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lindsvg", - "version": "1.3.3", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lindsvg", - "version": "1.3.3", + "version": "1.4.0", "license": "MIT", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", diff --git a/package.json b/package.json index 1210814..090c88a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.3.3", + "version": "1.4.0", "description": "Lindenmayer System [Scalable] Vector Graphics", "main": "./dist/lindsvg.js", "module": "./dist/lindsvg.esm.js", diff --git a/src/generator.mjs b/src/generator.mjs index 50d850e..cb92f10 100644 --- a/src/generator.mjs +++ b/src/generator.mjs @@ -7,6 +7,7 @@ let ctrlRules = { B: "", "+": "+", "-": "-", + "|": "|", "[": "[", "]": "]", }; @@ -26,7 +27,7 @@ let defaults = { */ function cleanCodeword(codeword) { // Remove auxiliary drawing-indifferent letters - let cleanCodeword = codeword.replace(/[^FB[\]+-]/g, ""); + let cleanCodeword = codeword.replace(/[^FB[\]+-|]/g, ""); do { codeword = cleanCodeword; // Remove useless brackets that don’t contain F commands or other brackets (preserving bracket balance!) @@ -59,5 +60,5 @@ export function generateCodeword(lsParams) { * @return {String[]} */ export function tokenizeCodeword(codeword) { - return codeword.match(/([FB[\]+-])\1*/g); // tokenize + return codeword.match(/([FB[\]+-|])\1*/g); // tokenize } diff --git a/src/svg.mjs b/src/svg.mjs index b152c3d..9980ed6 100644 --- a/src/svg.mjs +++ b/src/svg.mjs @@ -48,6 +48,9 @@ function getPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -97,6 +100,9 @@ function getMultiPathData(tokens, turtle) { case "-": turtle.rotate(-tokenLength); break; + case "|": + turtle.reverse(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/src/turtle.mjs b/src/turtle.mjs index 2367e52..e6b7f26 100644 --- a/src/turtle.mjs +++ b/src/turtle.mjs @@ -21,6 +21,10 @@ export class Turtle { this.alpha += factor * this.theta; } + reverse(repeatCount = 1) { + this.alpha += (repeatCount % 2) * Math.PI; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); diff --git a/src/validator.mjs b/src/validator.mjs index b0ced72..8efaec5 100644 --- a/src/validator.mjs +++ b/src/validator.mjs @@ -1,6 +1,6 @@ let messages = { - AXIOM: "Axiom may only contain the following characters: A..Z,+,-,[,]", - RULE: "Production rules may only contain the following characters: A..Z,+,-,[,]", + AXIOM: "Axiom may only contain the following characters: A..Z,+,-,|,[,]", + RULE: "Production rules may only contain the following characters: A..Z,+,-,|,[,]", LETTER: "Allowed alphabet letters are: A..Z", ALPHA: "The “alpha” parameter must be a finite number", THETA: "The “theta” parameter must be a finite number", @@ -14,7 +14,7 @@ export function checkLetter(letter, msg = messages.LETTER) { return letterRE.test(letter) || msg; } -let ruleRE = /^[A-Z+\-[\]]*$/; +let ruleRE = /^[A-Z+\-[\]|]*$/; export function checkRule(rule, msg = messages.RULE) { return ruleRE.test(rule) || msg; } diff --git a/test/install-test/package-lock.json b/test/install-test/package-lock.json index b68467a..e664e24 100644 --- a/test/install-test/package-lock.json +++ b/test/install-test/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "lindsvg": "file:../../lindsvg-1.3.3.tgz" + "lindsvg": "file:../../lindsvg-1.4.0.tgz" } }, "node_modules/lindsvg": { - "version": "1.3.3", - "resolved": "file:../../lindsvg-1.3.3.tgz", - "integrity": "sha512-ZVyUCjg96eoY4onY34/kzxnme8IR8hrrYgPb95Y2n7OkPiDqmMIapuqyka3dO4mOveFqRKgRT8QJQpXdOQHrvQ==", + "version": "1.4.0", + "resolved": "file:../../lindsvg-1.4.0.tgz", + "integrity": "sha512-yGtZ+UvdchTFbhDVUeNKDZP6cQ6SCos+3RjjY+a/ZabrbiU3pHAJ2mrxJ6+rj8guveh+/O42TMNpTTBKg3lILg==", "license": "MIT", "engines": { "node": ">=8.3.0" diff --git a/test/install-test/package.json b/test/install-test/package.json index 1df1d93..016fd24 100644 --- a/test/install-test/package.json +++ b/test/install-test/package.json @@ -9,6 +9,6 @@ "author": "Amphiluke", "license": "MIT", "dependencies": { - "lindsvg": "file:../../lindsvg-1.3.3.tgz" + "lindsvg": "file:../../lindsvg-1.4.0.tgz" } }