From 35f711b0629b443db734095ce12e5cc8ef7266e9 Mon Sep 17 00:00:00 2001 From: Amphiluke Date: Thu, 4 Jan 2024 15:21:05 +0700 Subject: [PATCH] =?UTF-8?q?Add=20support=20for=20the=20=E2=80=9Cswap=20sig?= =?UTF-8?q?n=E2=80=9D=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + dist/lindsvg.cjs | 23 +++++++++++++++++------ dist/lindsvg.esm.js | 23 +++++++++++++++++------ dist/lindsvg.esm.min.js | 4 ++-- dist/lindsvg.js | 23 +++++++++++++++++------ dist/lindsvg.min.js | 4 ++-- 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 +- test/params.mjs | 2 +- 15 files changed, 81 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 569bb63..343b7b1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The following turtle commands are currently supported by lindsvg: | `+` | Turn left by turning angle (theta) | | `-` | Turn right by turning angle (theta) | | `\|` | Reverse direction (turn by 180 degrees) | +| `!` | Reverse the meaning of `+` and `-` | | `[` | 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 | diff --git a/dist/lindsvg.cjs b/dist/lindsvg.cjs index 017af5d..a2eed52 100644 --- a/dist/lindsvg.cjs +++ b/dist/lindsvg.cjs @@ -1,13 +1,13 @@ /*! -lindsvg v1.4.0 +lindsvg v1.5.0 https://amphiluke.github.io/lindsvg/ (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; } @@ -118,6 +118,7 @@ let ctrlRules = { "+": "+", "-": "-", "|": "|", + "!": "!", "[": "[", "]": "]", }; @@ -137,7 +138,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!) @@ -170,7 +171,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 { @@ -200,6 +201,10 @@ class Turtle { this.alpha += (repeatCount % 2) * Math.PI; } + swapSigns(repeatCount = 1) { + this.theta *= (-1) ** repeatCount; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -271,6 +276,9 @@ function getPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -323,6 +331,9 @@ function getMultiPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.esm.js b/dist/lindsvg.esm.js index 6716570..12bca27 100644 --- a/dist/lindsvg.esm.js +++ b/dist/lindsvg.esm.js @@ -1,11 +1,11 @@ /*! -lindsvg v1.4.0 +lindsvg v1.5.0 https://amphiluke.github.io/lindsvg/ (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; } @@ -116,6 +116,7 @@ let ctrlRules = { "+": "+", "-": "-", "|": "|", + "!": "!", "[": "[", "]": "]", }; @@ -135,7 +136,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!) @@ -168,7 +169,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 { @@ -198,6 +199,10 @@ class Turtle { this.alpha += (repeatCount % 2) * Math.PI; } + swapSigns(repeatCount = 1) { + this.theta *= (-1) ** repeatCount; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -269,6 +274,9 @@ function getPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -321,6 +329,9 @@ function getMultiPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.esm.min.js b/dist/lindsvg.esm.min.js index 6cfaa23..0635445 100644 --- a/dist/lindsvg.esm.min.js +++ b/dist/lindsvg.esm.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.4.0 +lindsvg v1.5.0 https://amphiluke.github.io/lindsvg/ (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 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}; +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}swapSigns(t=1){this.theta*=(-1)**t}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 b(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.swapSigns(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 f(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"!":e.swapSigns(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 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 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}=b(t),{padding:h,width:c,height:o,pathAttributes:l}=x(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}=f(t),{padding:h,width:c,height:o,pathAttributes:l}=x(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,f as getMultiPathSVGData,w as getSVGCode,b as getSVGData}; diff --git a/dist/lindsvg.js b/dist/lindsvg.js index 4d58265..905d63a 100644 --- a/dist/lindsvg.js +++ b/dist/lindsvg.js @@ -1,5 +1,5 @@ /*! -lindsvg v1.4.0 +lindsvg v1.5.0 https://amphiluke.github.io/lindsvg/ (c) 2024 Amphiluke */ @@ -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; } @@ -122,6 +122,7 @@ https://amphiluke.github.io/lindsvg/ "+": "+", "-": "-", "|": "|", + "!": "!", "[": "[", "]": "]", }; @@ -141,7 +142,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!) @@ -174,7 +175,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 { @@ -204,6 +205,10 @@ https://amphiluke.github.io/lindsvg/ this.alpha += (repeatCount % 2) * Math.PI; } + swapSigns(repeatCount = 1) { + this.theta *= (-1) ** repeatCount; + } + pushStack(repeatCount = 1) { for (; repeatCount > 0; repeatCount--) { this.stack.push({x: this.x, y: this.y, alpha: this.alpha}); @@ -275,6 +280,9 @@ https://amphiluke.github.io/lindsvg/ case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -327,6 +335,9 @@ https://amphiluke.github.io/lindsvg/ case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/dist/lindsvg.min.js b/dist/lindsvg.min.js index 3907623..2ff6b1a 100644 --- a/dist/lindsvg.min.js +++ b/dist/lindsvg.min.js @@ -1,6 +1,6 @@ /*! -lindsvg v1.4.0 +lindsvg v1.5.0 https://amphiluke.github.io/lindsvg/ (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"},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})); +!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}swapSigns(t=1){this.theta*=(-1)**t}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.swapSigns(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"!":e.swapSigns(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 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 x({viewBox:t,width:e,height:a,content:i}){return`${i}`}function w(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}=b(e,n,s);return x({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}=b(e,n,s);return x({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 8367301..159861f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lindsvg", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lindsvg", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", diff --git a/package.json b/package.json index 090c88a..460ac0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lindsvg", - "version": "1.4.0", + "version": "1.5.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 cb92f10..d0b1e2b 100644 --- a/src/generator.mjs +++ b/src/generator.mjs @@ -8,6 +8,7 @@ let ctrlRules = { "+": "+", "-": "-", "|": "|", + "!": "!", "[": "[", "]": "]", }; @@ -27,7 +28,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!) @@ -60,5 +61,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 9980ed6..7fb0120 100644 --- a/src/svg.mjs +++ b/src/svg.mjs @@ -51,6 +51,9 @@ function getPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": turtle.pushStack(tokenLength); break; @@ -103,6 +106,9 @@ function getMultiPathData(tokens, turtle) { case "|": turtle.reverse(tokenLength); break; + case "!": + turtle.swapSigns(tokenLength); + break; case "[": branchLevel += tokenLength; turtle.pushStack(tokenLength); diff --git a/src/turtle.mjs b/src/turtle.mjs index e6b7f26..f58bd3a 100644 --- a/src/turtle.mjs +++ b/src/turtle.mjs @@ -25,6 +25,10 @@ export class Turtle { this.alpha += (repeatCount % 2) * Math.PI; } + swapSigns(repeatCount = 1) { + this.theta *= (-1) ** repeatCount; + } + 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 8efaec5..c3a821f 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 e664e24..4f69de2 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.4.0.tgz" + "lindsvg": "file:../../lindsvg-1.5.0.tgz" } }, "node_modules/lindsvg": { - "version": "1.4.0", - "resolved": "file:../../lindsvg-1.4.0.tgz", - "integrity": "sha512-yGtZ+UvdchTFbhDVUeNKDZP6cQ6SCos+3RjjY+a/ZabrbiU3pHAJ2mrxJ6+rj8guveh+/O42TMNpTTBKg3lILg==", + "version": "1.5.0", + "resolved": "file:../../lindsvg-1.5.0.tgz", + "integrity": "sha512-JbB9J43ON4CtiVw0axPnl6SLEX585iZ2jPTzH7LzlJVaID5RHA1pPTREUy42Q3ToW0i2ZtWaYwScR0p3frsRkg==", "license": "MIT", "engines": { "node": ">=8.3.0" diff --git a/test/install-test/package.json b/test/install-test/package.json index 016fd24..04eb55a 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.4.0.tgz" + "lindsvg": "file:../../lindsvg-1.5.0.tgz" } } diff --git a/test/params.mjs b/test/params.mjs index 225c5e5..b8e0b5c 100644 --- a/test/params.mjs +++ b/test/params.mjs @@ -11,7 +11,7 @@ export let singlePathLSParams = { }; export let multiPathLSParams = { - axiom: "FFF+FFFF-FF+FF-[-Y][+Y][Z][+Z]", + axiom: "FFF+FFFF-FF+FF-[-Y][+Y][!Z!][+Z]", rules: { F: "F", Y: "FF+F-F-F[FFFZ][+Z]-F-FZ",