Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimize walking of L-path vertices #300

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions src/common/Graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that you're adding both nodes each time? You would have to handle the edge case if you didn't do it this way, but it would save almost half the work on large lists of nodes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that you're adding both nodes each time? You would have to handle the edge case if you didn't do it this way, but it would save almost half the work on large lists of nodes.

You're right. addNode handles it, but that's sloppy and I'll fix.

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() {
Expand Down Expand Up @@ -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()
}
}
Expand All @@ -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)

Expand Down
53 changes: 53 additions & 0 deletions src/common/lindenmayer.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -122,3 +169,9 @@ export const lsystemPath = (instructions, config) => {

return currVertices
}

export const lsystemOptimize = (vertices, config) => {
return config.shortestPath >= config.iterations
? shortestPath(vertices)
: vertices
}
5 changes: 3 additions & 2 deletions src/features/shapes/lsystem/LSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Shape from "../Shape"
import {
lsystem,
lsystemPath,
lsystemOptimize,
onSubtypeChange,
onMinIterations,
onMaxIterations,
Expand Down Expand Up @@ -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() {
Expand Down
10 changes: 7 additions & 3 deletions src/features/shapes/lsystem/subtypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/features/shapes/space_filler/SpaceFiller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Shape from "../Shape"
import {
lsystem,
lsystemPath,
lsystemOptimize,
onSubtypeChange,
onMinIterations,
onMaxIterations,
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/features/shapes/space_filler/subtypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down
Loading