diff --git a/src/common/Graph.js b/src/common/Graph.js index 3c1f3ed..b8f0322 100644 --- a/src/common/Graph.js +++ b/src/common/Graph.js @@ -13,6 +13,30 @@ export const mix = (v1, v2, s) => { return new Victor(result[0], result[1]) } +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(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 +66,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 +86,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/common/lindenmayer.js b/src/common/lindenmayer.js index 0d10ba1..441b44d 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 df5ace9..d3aa567 100644 --- a/src/features/shapes/lsystem/LSystem.js +++ b/src/features/shapes/lsystem/LSystem.js @@ -2,6 +2,7 @@ import Shape from "../Shape" import { lsystem, lsystemPath, + lsystemOptimize, onSubtypeChange, onMinIterations, onMaxIterations, @@ -63,10 +64,10 @@ export default class LSystem extends Shape { config.angle = Math.PI / 2 } - let curve = lsystemPath(lsystem(config), config) + const path = lsystemOptimize(lsystemPath(lsystem(config), config), config) const scale = 18.0 // to normalize starting size - return resizeVertices(curve, scale, scale) + return resizeVertices(path, scale, scale) } getOptions() { diff --git a/src/features/shapes/lsystem/subtypes.js b/src/features/shapes/lsystem/subtypes.js index 1fa6913..932d2cb 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", diff --git a/src/features/shapes/space_filler/SpaceFiller.js b/src/features/shapes/space_filler/SpaceFiller.js index 7e31437..0371403 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 dc54298..521985a 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) },