From dcc440fe68156df9b0abf718dfe12f62dfebb98f Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 12 Oct 2024 07:11:19 -0400 Subject: [PATCH 1/5] don't sanitize PRE and POST code --- src/features/export/Exporter.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/export/Exporter.js b/src/features/export/Exporter.js index 551e10d2..d38043dc 100644 --- a/src/features/export/Exporter.js +++ b/src/features/export/Exporter.js @@ -77,7 +77,7 @@ export default class Exporter { this.startComments() this.line("BEGIN PRE") this.endComments() - this.line(this.pre, this.pre !== "") + this.line(this.pre, this.pre !== "", false) this.startComments() this.line("END PRE") this.endComments() @@ -91,7 +91,7 @@ export default class Exporter { this.startComments() this.line("BEGIN POST") this.endComments() - this.line(this.post, this.post !== "") + this.line(this.post, this.post !== "", false) this.startComments() this.line("END POST") this.endComments() @@ -115,7 +115,7 @@ export default class Exporter { this.vertices = vertices } - line(content = "", add = true) { + line(content = "", add = true, sanitize = true) { if (add) { let padding = "" if (this.commenting) { @@ -124,7 +124,10 @@ export default class Exporter { padding += " " } } - this.lines.push(padding + this.sanitizeValue(content)) + + const preparedContent = sanitize ? this.sanitizeValue(content) : content + + this.lines.push(padding + preparedContent) } } From 16c04058e975e6765e758546f96ec3d0fd5c8a35 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 12 Oct 2024 11:13:39 -0400 Subject: [PATCH 2/5] walk shortest paths in l-systems where useful and not too computationally intensive --- src/common/Graph.js | 33 ++++++++++++-- src/features/shapes/lsystem/LSystem.js | 59 +++++++++++++++++++++++-- src/features/shapes/lsystem/subtypes.js | 10 +++-- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/common/Graph.js b/src/common/Graph.js index 3c1f3edb..3f92050a 100644 --- a/src/common/Graph.js +++ b/src/common/Graph.js @@ -13,6 +13,27 @@ export const mix = (v1, v2, s) => { return new Victor(result[0], result[1]) } +export const buildGraph = (nodes) => { + const graph = new Graph() + + for (let i = 0; i < nodes.length - 1; i++) { + const node1 = nodes[i] + const node2 = nodes[i + 1] + graph.addNode(node1) + graph.addNode(node2) + graph.addEdge(node1, node2) + } + + return graph +} + +export const edgeKey = (node1, node2) => { + const node1Key = node1.toString() + const node2Key = node2.toString() + + return [node1Key, node2Key].sort().toString() +} + // note: requires string-based nodes to work properly export default class Graph { constructor() { @@ -42,13 +63,13 @@ export default class Graph { addEdge(node1, node2, weight = 1) { let node1Key = node1.toString() let node2Key = node2.toString() - let edgeKey = [node1Key, node2Key].sort().toString() + let edge12Key = edgeKey(node1, node2) - if (!this.edgeKeys.has(edgeKey)) { + if (!this.edgeKeys.has(edge12Key)) { this.adjacencyList[node1Key].push({ node: node2, weight }) this.adjacencyList[node2Key].push({ node: node1, weight }) - this.edgeKeys.add(edgeKey) - this.edgeMap[edgeKey] = [node1.toString(), node2.toString()] + this.edgeKeys.add(edge12Key) + this.edgeMap[edge12Key] = [node1.toString(), node2.toString()] this.clearCachedPaths() } } @@ -62,6 +83,10 @@ export default class Graph { return this.adjacencyList[node.toString()].map((hash) => hash.node) } + getNode(node) { + return this.nodeMap[node.toString()] + } + dijkstraShortestPath(startNode, endNode) { let shortest = this.getCachedShortestPath(startNode, endNode) diff --git a/src/features/shapes/lsystem/LSystem.js b/src/features/shapes/lsystem/LSystem.js index df5ace94..d5123aa3 100644 --- a/src/features/shapes/lsystem/LSystem.js +++ b/src/features/shapes/lsystem/LSystem.js @@ -7,7 +7,8 @@ import { onMaxIterations, } from "@/common/lindenmayer" import { subtypes } from "./subtypes" -import { resizeVertices } from "@/common/geometry" +import { resizeVertices, cloneVertices } from "@/common/geometry" +import { buildGraph, edgeKey } from "@/common/Graph" const options = { subtype: { @@ -63,13 +64,65 @@ export default class LSystem extends Shape { config.angle = Math.PI / 2 } - let curve = lsystemPath(lsystem(config), config) + const curve = lsystemPath(lsystem(config), config) const scale = 18.0 // to normalize starting size + const path = + config.shortestPath >= iterations ? this.shortestPath(curve) : curve - return resizeVertices(curve, scale, scale) + return resizeVertices(path, scale, scale) } getOptions() { return options } + + shortestPath(nodes) { + const graph = buildGraph(nodes) + const path = [] + const visited = {} + + for (let i = 0; i < nodes.length - 1; i++) { + const node1 = nodes[i] + const node2 = nodes[i + 1] + let node1Key = node1.toString() + let edge12Key = edgeKey(node1, node2) + + if (visited[edge12Key]) { + const unvisitedNode = this.nearestUnvisitedNode( + i + 1, + nodes, + visited, + graph, + ) + + if (unvisitedNode != null) { + const shortestSubPath = graph.dijkstraShortestPath( + node1Key, + unvisitedNode.toString(), + ) + + path.push(...cloneVertices(shortestSubPath.slice(1))) + i = nodes.indexOf(unvisitedNode) - 1 + } + } else { + path.push(node2) + visited[edge12Key] = true + } + } + + return path + } + + nearestUnvisitedNode(nodeIndex, nodes, visited, graph) { + for (let i = nodeIndex; i < nodes.length - 1; i++) { + const node1 = nodes[i] + const node2 = nodes[i + 1] + + if (!visited[edgeKey(node1, node2)]) { + return node2 + } + } + + return null // all nodes visited + } } diff --git a/src/features/shapes/lsystem/subtypes.js b/src/features/shapes/lsystem/subtypes.js index 1fa69137..932d2cbd 100644 --- a/src/features/shapes/lsystem/subtypes.js +++ b/src/features/shapes/lsystem/subtypes.js @@ -106,7 +106,8 @@ export const subtypes = { rules: { F: "FF-F-F-F-FF", }, - maxIterations: 5, + maxIterations: 4, + shortestPath: 3, }, // http://algorithmicbotany.org/papers/abop/abop-ch1.pdf "Koch Cube 2": { @@ -115,7 +116,8 @@ export const subtypes = { rules: { F: "FF-F+F-F-FF", }, - maxIterations: 5, + maxIterations: 4, + shortestPath: 3, }, // https://onlinemathtools.com/l-system-generator "Koch Curve": { @@ -136,6 +138,7 @@ export const subtypes = { F: "FF-F-F-F-F-F+F", }, maxIterations: 4, + shortestPath: 3, }, // http://mathforum.org/advanced/robertd/lsys2d.html "Koch Island": { @@ -178,7 +181,8 @@ export const subtypes = { 9: "--8++++6[+9++++7]--7", }, angle: Math.PI / 5, - maxIterations: 6, + maxIterations: 5, + shortestPath: 5, }, Plusses: { axiom: "XYXYXYX+XYXYXYX+XYXYXYX+XYXYXYX", From 5d097fb05041262db8a14d38fccbf2ea2186a5ed Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Tue, 15 Oct 2024 17:05:04 -0400 Subject: [PATCH 3/5] avoid double-adding nodes --- src/common/Graph.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/Graph.js b/src/common/Graph.js index 3f92050a..b8f03220 100644 --- a/src/common/Graph.js +++ b/src/common/Graph.js @@ -16,10 +16,13 @@ export const mix = (v1, v2, s) => { export const buildGraph = (nodes) => { const graph = new Graph() + if (nodes.length > 0) { + graph.addNode(nodes[0]) + } + for (let i = 0; i < nodes.length - 1; i++) { const node1 = nodes[i] const node2 = nodes[i + 1] - graph.addNode(node1) graph.addNode(node2) graph.addEdge(node1, node2) } From ff169aae74da0584afe980cf7029eeb2bb1ce395 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Tue, 15 Oct 2024 20:03:08 -0400 Subject: [PATCH 4/5] optimize space filler, too --- src/common/lindenmayer.js | 53 +++++++++++++++++ src/features/shapes/lsystem/LSystem.js | 58 +------------------ .../shapes/space_filler/SpaceFiller.js | 3 +- src/features/shapes/space_filler/subtypes.js | 3 +- 4 files changed, 60 insertions(+), 57 deletions(-) diff --git a/src/common/lindenmayer.js b/src/common/lindenmayer.js index 0d10ba14..441b44d8 100644 --- a/src/common/lindenmayer.js +++ b/src/common/lindenmayer.js @@ -1,5 +1,52 @@ import Victor from "victor" import { vertexRoundP, cloneVertex } from "./geometry" +import { buildGraph, edgeKey } from "@/common/Graph" +import { cloneVertices } from "@/common/geometry" + +const shortestPath = (nodes) => { + const graph = buildGraph(nodes) + const path = [] + const visited = {} + + for (let i = 0; i < nodes.length - 1; i++) { + const node1 = nodes[i] + const node2 = nodes[i + 1] + let node1Key = node1.toString() + let edge12Key = edgeKey(node1, node2) + + if (visited[edge12Key]) { + const unvisitedNode = nearestUnvisitedNode(i + 1, nodes, visited, graph) + + if (unvisitedNode != null) { + const shortestSubPath = graph.dijkstraShortestPath( + node1Key, + unvisitedNode.toString(), + ) + + path.push(...cloneVertices(shortestSubPath.slice(1))) + i = nodes.indexOf(unvisitedNode) - 1 + } + } else { + path.push(node2) + visited[edge12Key] = true + } + } + + return path +} + +const nearestUnvisitedNode = (nodeIndex, nodes, visited, graph) => { + for (let i = nodeIndex; i < nodes.length - 1; i++) { + const node1 = nodes[i] + const node2 = nodes[i + 1] + + if (!visited[edgeKey(node1, node2)]) { + return node2 + } + } + + return null // all nodes visited +} export const onSubtypeChange = (subtype, changes, attrs) => { // if we switch back with too many iterations, the code @@ -122,3 +169,9 @@ export const lsystemPath = (instructions, config) => { return currVertices } + +export const lsystemOptimize = (vertices, config) => { + return config.shortestPath >= config.iterations + ? shortestPath(vertices) + : vertices +} diff --git a/src/features/shapes/lsystem/LSystem.js b/src/features/shapes/lsystem/LSystem.js index d5123aa3..d3aa5679 100644 --- a/src/features/shapes/lsystem/LSystem.js +++ b/src/features/shapes/lsystem/LSystem.js @@ -2,13 +2,13 @@ import Shape from "../Shape" import { lsystem, lsystemPath, + lsystemOptimize, onSubtypeChange, onMinIterations, onMaxIterations, } from "@/common/lindenmayer" import { subtypes } from "./subtypes" -import { resizeVertices, cloneVertices } from "@/common/geometry" -import { buildGraph, edgeKey } from "@/common/Graph" +import { resizeVertices } from "@/common/geometry" const options = { subtype: { @@ -64,10 +64,8 @@ export default class LSystem extends Shape { config.angle = Math.PI / 2 } - const curve = lsystemPath(lsystem(config), config) + const path = lsystemOptimize(lsystemPath(lsystem(config), config), config) const scale = 18.0 // to normalize starting size - const path = - config.shortestPath >= iterations ? this.shortestPath(curve) : curve return resizeVertices(path, scale, scale) } @@ -75,54 +73,4 @@ export default class LSystem extends Shape { getOptions() { return options } - - shortestPath(nodes) { - const graph = buildGraph(nodes) - const path = [] - const visited = {} - - for (let i = 0; i < nodes.length - 1; i++) { - const node1 = nodes[i] - const node2 = nodes[i + 1] - let node1Key = node1.toString() - let edge12Key = edgeKey(node1, node2) - - if (visited[edge12Key]) { - const unvisitedNode = this.nearestUnvisitedNode( - i + 1, - nodes, - visited, - graph, - ) - - if (unvisitedNode != null) { - const shortestSubPath = graph.dijkstraShortestPath( - node1Key, - unvisitedNode.toString(), - ) - - path.push(...cloneVertices(shortestSubPath.slice(1))) - i = nodes.indexOf(unvisitedNode) - 1 - } - } else { - path.push(node2) - visited[edge12Key] = true - } - } - - return path - } - - nearestUnvisitedNode(nodeIndex, nodes, visited, graph) { - for (let i = nodeIndex; i < nodes.length - 1; i++) { - const node1 = nodes[i] - const node2 = nodes[i + 1] - - if (!visited[edgeKey(node1, node2)]) { - return node2 - } - } - - return null // all nodes visited - } } diff --git a/src/features/shapes/space_filler/SpaceFiller.js b/src/features/shapes/space_filler/SpaceFiller.js index 7e314370..0371403c 100644 --- a/src/features/shapes/space_filler/SpaceFiller.js +++ b/src/features/shapes/space_filler/SpaceFiller.js @@ -2,6 +2,7 @@ import Shape from "../Shape" import { lsystem, lsystemPath, + lsystemOptimize, onSubtypeChange, onMinIterations, onMaxIterations, @@ -86,7 +87,7 @@ export default class SpaceFiller extends Shape { config.angle = Math.PI / 2 } - let curve = lsystemPath(lsystem(config), config) + let curve = lsystemOptimize(lsystemPath(lsystem(config), config), config) let scale = 1 if (config.iterationsGrow) { diff --git a/src/features/shapes/space_filler/subtypes.js b/src/features/shapes/space_filler/subtypes.js index dc54298d..521985a3 100644 --- a/src/features/shapes/space_filler/subtypes.js +++ b/src/features/shapes/space_filler/subtypes.js @@ -58,7 +58,8 @@ export const subtypes = { 9: "--8++++6[+9++++7]--7", }, angle: Math.PI / 5, - maxIterations: 6, + maxIterations: 5, + shortestPath: 5, iterationsGrow: (config) => { return 1 + Math.max(1, 3 / config.iterations) }, From 43a4e78a63b3b797a4c57689ea067593acac1d94 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Tue, 15 Oct 2024 20:08:57 -0400 Subject: [PATCH 5/5] remove exporter change (in another PR) --- src/features/export/Exporter.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/features/export/Exporter.js b/src/features/export/Exporter.js index d38043dc..551e10d2 100644 --- a/src/features/export/Exporter.js +++ b/src/features/export/Exporter.js @@ -77,7 +77,7 @@ export default class Exporter { this.startComments() this.line("BEGIN PRE") this.endComments() - this.line(this.pre, this.pre !== "", false) + this.line(this.pre, this.pre !== "") this.startComments() this.line("END PRE") this.endComments() @@ -91,7 +91,7 @@ export default class Exporter { this.startComments() this.line("BEGIN POST") this.endComments() - this.line(this.post, this.post !== "", false) + this.line(this.post, this.post !== "") this.startComments() this.line("END POST") this.endComments() @@ -115,7 +115,7 @@ export default class Exporter { this.vertices = vertices } - line(content = "", add = true, sanitize = true) { + line(content = "", add = true) { if (add) { let padding = "" if (this.commenting) { @@ -124,10 +124,7 @@ export default class Exporter { padding += " " } } - - const preparedContent = sanitize ? this.sanitizeValue(content) : content - - this.lines.push(padding + preparedContent) + this.lines.push(padding + this.sanitizeValue(content)) } }