@painekillers/tree-builder-core 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/graph.js ADDED
@@ -0,0 +1,339 @@
1
+ export class Node {
2
+ constructor(id, pa, ch, val){
3
+ this.id = id
4
+ this.pa = pa
5
+ this.ch = ch
6
+ this.val = val
7
+
8
+ this.parents = []
9
+ this.children = []
10
+ }
11
+ }
12
+
13
+ export class Graph {
14
+ constructor(nodes){
15
+ this.nodes = new Map(nodes.map(n => [n.id, n]))
16
+
17
+ if (this.nodes.size !== nodes.length) {
18
+ console.warn(`Duplicate node IDs`)
19
+ }
20
+
21
+ // resolve parent and child references
22
+ for (const node of this.nodes.values()) {
23
+ const parentIds = node.pa;
24
+ const childIds = node.ch;
25
+
26
+ // node says X is its parent
27
+ for (const parentId of parentIds) {
28
+ const parent = this.nodeFromId(parentId);
29
+ if(!parent) {
30
+ console.warn(`Parent with id ${parentId} not found for node ${node.id}`);
31
+ continue;
32
+ }
33
+
34
+ if (!node.parents.includes(parent)) {
35
+ node.parents.push(parent);
36
+ }
37
+
38
+ if (!parent.children.includes(node)) {
39
+ parent.children.push(node);
40
+ }
41
+ }
42
+
43
+ // node says X is its child
44
+ for (const childId of childIds) {
45
+ const child = this.nodeFromId(childId);
46
+ if(!child) {
47
+ console.warn(`Child with id ${childId} not found for node ${node.id}`);
48
+ continue;
49
+ }
50
+
51
+ if (!node.children.includes(child)) {
52
+ node.children.push(child);
53
+ }
54
+
55
+ if (!child.parents.includes(node)) {
56
+ child.parents.push(node);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ nodeFromId(id) {
63
+ return this.nodes.get(id)
64
+ }
65
+
66
+ }
67
+
68
+ export class LayoutNode {
69
+ constructor(node, l, original = null) {
70
+ this.node = node
71
+ this.connected = []
72
+
73
+ this.layer = l
74
+ this.original = original
75
+ }
76
+
77
+ addConnection(c){
78
+ this.connected.push(c)
79
+ }
80
+ }
81
+
82
+ /*
83
+ Recursion settings
84
+ Null: No recursion
85
+ False: Creates fake nodes when encountering recursion
86
+ True: Connects back to existing instance of a node when recursing
87
+ #: Real recursion to depths #
88
+ */
89
+
90
+ /*
91
+ Strategies
92
+ - [X] Current BFS (First) — Layer by shortest distance from root; first discovery determines placement.
93
+ - [X] Undirected BFS (Undirected) — Shortest distance to root; layering is on directional.
94
+ - [X] Directed BFS, outward only (Outwards) — Follow prerequisite-to-dependent edges only.
95
+ - [X] Furthest-parent layering (Furthest) — Place each node one layer below its deepest prerequisite.
96
+ */
97
+
98
+ export class LayoutGraph {
99
+ constructor(graph, root, recurse, strategy = "First") {
100
+ if(strategy == "First" && typeof recurse == "number") {
101
+ throw new Error(`Cannot use strategy ${strategy} with numeric recursion setting`);
102
+ }
103
+
104
+ this.layers = new Map()
105
+
106
+ const rootNode = graph.nodeFromId(root)
107
+ if(!rootNode) {
108
+ throw new Error(`Root node with id ${root} not found in graph`);
109
+ }
110
+
111
+ if(recurse === 0) {
112
+ this.layers.set(0, [new LayoutNode(rootNode, 0)])
113
+ return
114
+ }
115
+
116
+ const visited = new Map()
117
+ const connections = new Set()
118
+
119
+ const addConnection = (ln, from) => {
120
+ const fromNode =
121
+ this.layers.get(from.layer)?.[from.ind]
122
+
123
+ if(!fromNode) return
124
+
125
+ const a = `${ln.layer}:${ln.ind}`
126
+ const b = `${from.layer}:${from.ind}`
127
+
128
+ const key = [a, b].sort().join("|")
129
+
130
+ if(connections.has(key)) {
131
+ return
132
+ }
133
+
134
+ connections.add(key)
135
+ ln.addConnection(from)
136
+ }
137
+
138
+ const queue = [{
139
+ node: rootNode,
140
+ layer: 0,
141
+ from: null
142
+ }]
143
+
144
+ let qi = 0
145
+
146
+ while (qi < queue.length) {
147
+ const { node, layer, from } = queue[qi++]
148
+
149
+ if(typeof recurse == "number" && Math.abs(layer) > recurse) {
150
+ continue
151
+ }
152
+
153
+ if(!this.layers.has(layer)) {
154
+ this.layers.set(layer, [])
155
+ }
156
+
157
+ const key = typeof recurse == "number"
158
+ ? `${node.id}|${layer}`
159
+ : node.id
160
+
161
+ if(visited.has(key)) {
162
+ if(from) {
163
+ const pos = visited.get(key)
164
+ const ln = this.layers.get(pos.layer)[pos.ind]
165
+
166
+ //holy crap i cannot think bruh
167
+ if(((recurse === false) || (recurse === null)) && pos.layer !== layer) {
168
+ if (
169
+ recurse == null ||
170
+ strategy == "Outwards" &&
171
+ from !== null &&
172
+ Math.abs(layer) < Math.abs(from.layer)
173
+ ) { continue }
174
+
175
+ if (
176
+ strategy == "Furthest" &&
177
+ (
178
+ (layer > 0 && !node.parents.every(val => visited.has(val.id)))
179
+ ||
180
+ (layer < 0 && !node.children.every(val => visited.has(val.id)))
181
+ )
182
+ ) { continue }
183
+
184
+ const fn = new LayoutNode(node, layer, this.layers.get(pos.layer)[pos.ind])
185
+ this.layers.get(layer).push(fn)
186
+
187
+ addConnection(fn, from)
188
+ } else {
189
+ addConnection(ln, from)
190
+
191
+ }
192
+ }
193
+
194
+ continue
195
+ }
196
+
197
+ if (
198
+ strategy == "Outwards" &&
199
+ from !== null &&
200
+ Math.abs(layer) < Math.abs(from.layer)
201
+ ) { continue }
202
+
203
+ if (
204
+ strategy == "Furthest" &&
205
+ (
206
+ (layer > 0 && !node.parents.every(val => visited.has(val.id)))
207
+ ||
208
+ (layer < 0 && !node.children.every(val => visited.has(val.id)))
209
+ )
210
+ ) { continue }
211
+
212
+ const ln = new LayoutNode(node, layer)
213
+
214
+ this.layers.get(layer).push(ln)
215
+
216
+ const pos = {
217
+ layer,
218
+ ind: this.layers.get(layer).length - 1
219
+ }
220
+
221
+ visited.set(key, pos)
222
+
223
+ if(from) {
224
+ addConnection(ln, from)
225
+ }
226
+
227
+ for (const child of node.children) {
228
+ queue.push({
229
+ node: child,
230
+ layer: layer + 1,
231
+ from: pos
232
+ })
233
+ }
234
+
235
+ for (const parent of node.parents) {
236
+ queue.push({
237
+ node: parent,
238
+ layer: strategy !== "Undirected" ? layer - 1: layer + 1,
239
+ from: pos
240
+ })
241
+ }
242
+
243
+ }
244
+ }
245
+ }
246
+
247
+ export class DrawNode {
248
+ constructor(layoutNode, x, y, width, height) {
249
+ this.layoutNode = layoutNode
250
+ this.x = x
251
+ this.y = y
252
+ this.width = width
253
+ this.height = height
254
+ }
255
+ }
256
+
257
+ export class DrawConnection {
258
+ constructor(fromNode, toNode) {
259
+ this.fromNode = fromNode
260
+ this.toNode = toNode
261
+
262
+ if (fromNode.y <= toNode.y) {
263
+ this.from = {
264
+ x: fromNode.x,
265
+ y: fromNode.y + fromNode.height / 2
266
+ }
267
+
268
+ this.to = {
269
+ x: toNode.x,
270
+ y: toNode.y - toNode.height / 2
271
+ }
272
+ } else {
273
+ this.from = {
274
+ x: fromNode.x,
275
+ y: fromNode.y - fromNode.height / 2
276
+ }
277
+
278
+ this.to = {
279
+ x: toNode.x,
280
+ y: toNode.y + toNode.height / 2
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ export class DrawGraph {
287
+ constructor(layoutGraph, nodeWidth, nodeHeight, layerSpacing, nodeSpacing) {
288
+ this.nodes = []
289
+ this.connections = []
290
+
291
+ const drawNodes = new Map()
292
+
293
+ const layers = Array.from(layoutGraph.layers.entries())
294
+ .sort((a, b) => a[0] - b[0])
295
+
296
+ for (const [layerIndex, layerNodes] of layers) {
297
+ const layerWidth =
298
+ layerNodes.length * nodeWidth +
299
+ (layerNodes.length - 1) * nodeSpacing
300
+
301
+ let x = -layerWidth / 2 + nodeWidth / 2
302
+
303
+ const y =
304
+ layerIndex * (nodeHeight + layerSpacing)
305
+
306
+ for (const layoutNode of layerNodes) {
307
+ const drawNode = new DrawNode(
308
+ layoutNode,
309
+ x,
310
+ y,
311
+ nodeWidth,
312
+ nodeHeight
313
+ )
314
+
315
+ this.nodes.push(drawNode)
316
+ drawNodes.set(layoutNode, drawNode)
317
+
318
+ x += nodeWidth + nodeSpacing
319
+ }
320
+ }
321
+
322
+ for (const drawNode of this.nodes) {
323
+ for (const connection of drawNode.layoutNode.connected) {
324
+ const toLayoutNode =
325
+ layoutGraph.layers
326
+ .get(connection.layer)[connection.ind]
327
+
328
+ const toDrawNode =
329
+ drawNodes.get(toLayoutNode)
330
+
331
+ if (toDrawNode) {
332
+ this.connections.push(
333
+ new DrawConnection(drawNode, toDrawNode)
334
+ )
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as Bridge } from "./world/bridge.js"
2
+ export { default as Camera } from "./world/camera.js"
3
+ export { default as World } from "./world/world.js"
4
+
5
+ export { Node, Graph } from "./graph.js"
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@painekillers/tree-builder-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./index.js",
7
+ "./themes/*": "./themes/*"
8
+ }
9
+ }
package/themes/dark.js ADDED
@@ -0,0 +1,154 @@
1
+ export default {
2
+ background: Object.assign(new Image(),
3
+ {
4
+ src: "data:image/svg+xml," + encodeURIComponent(`
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080">
6
+ <defs>
7
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
8
+ <path
9
+ d="M 40 0 L 0 0 0 40"
10
+ fill="none"
11
+ stroke="#3f4147"
12
+ stroke-width="1"
13
+ opacity="0.35"
14
+ />
15
+ </pattern>
16
+ </defs>
17
+
18
+ <rect
19
+ width="1920"
20
+ height="1080"
21
+ fill="#111112"
22
+ />
23
+
24
+ <rect
25
+ width="1920"
26
+ height="1080"
27
+ fill="url(#grid)"
28
+ />
29
+ </svg>
30
+ `)
31
+ }
32
+ ),
33
+
34
+ draw: {
35
+ line(ctx, object) {
36
+ ctx.save()
37
+
38
+ ctx.lineCap = "round"
39
+
40
+ ctx.beginPath()
41
+
42
+ ctx.moveTo(
43
+ object.from.x,
44
+ object.from.y
45
+ )
46
+
47
+ ctx.lineTo(
48
+ object.to.x,
49
+ object.to.y
50
+ )
51
+
52
+ ctx.lineWidth = object.width
53
+ ctx.strokeStyle = "#4b4d52"
54
+ ctx.stroke()
55
+
56
+ ctx.restore()
57
+ },
58
+
59
+ node(ctx, object) {
60
+ const pos = object.pos
61
+ const size = object.size
62
+
63
+ const width = size.width * object.scale
64
+ const height = size.height * object.scale
65
+
66
+ const x = pos.x - width / 2
67
+ const y = pos.y - height / 2
68
+
69
+ const radius = 10
70
+
71
+ const fake = object.drawNode.layoutNode.original !== null
72
+
73
+ ctx.save()
74
+
75
+ ctx.shadowColor = "rgba(0, 0, 0, 0.35)"
76
+ ctx.shadowBlur = object.hovered ? 14 : 7
77
+ ctx.shadowOffsetY = object.hovered ? 3 : 2
78
+
79
+ ctx.fillStyle = "#2b2d31"
80
+
81
+ ctx.beginPath()
82
+ ctx.roundRect(
83
+ x,
84
+ y,
85
+ width,
86
+ height,
87
+ radius
88
+ )
89
+ ctx.fill()
90
+
91
+ ctx.shadowColor = "transparent"
92
+
93
+ ctx.save()
94
+
95
+ ctx.beginPath()
96
+ ctx.roundRect(
97
+ x,
98
+ y,
99
+ width,
100
+ height,
101
+ radius
102
+ )
103
+ ctx.clip()
104
+
105
+ ctx.globalAlpha = object.hovered ? 1 : 0.92
106
+
107
+ ctx.drawImage(
108
+ object.image,
109
+ x,
110
+ y,
111
+ width,
112
+ height
113
+ )
114
+
115
+ ctx.restore()
116
+
117
+ ctx.beginPath()
118
+ ctx.roundRect(
119
+ x,
120
+ y,
121
+ width,
122
+ height,
123
+ radius
124
+ )
125
+
126
+ ctx.lineWidth = 1
127
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.08)"
128
+
129
+ if(fake) {
130
+ ctx.setLineDash([4, 4])
131
+ }
132
+
133
+ ctx.stroke()
134
+ ctx.setLineDash([])
135
+
136
+ if (object.root) {
137
+ ctx.beginPath()
138
+
139
+ ctx.arc(
140
+ x + width - 9,
141
+ y + 9,
142
+ 3,
143
+ 0,
144
+ Math.PI * 2
145
+ )
146
+
147
+ ctx.fillStyle = "rgba(255, 255, 255, 0.35)"
148
+ ctx.fill()
149
+ }
150
+
151
+ ctx.restore()
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,240 @@
1
+ const animate = node => {
2
+ if(node.animation) return
3
+
4
+ const animate = () => {
5
+ const target = node.hovered ? 1.04 : 1
6
+ const difference = target - node.scale
7
+
8
+ if(Math.abs(difference) < 0.001) {
9
+ node.scale = target
10
+ node.animation = null
11
+ node.world.update()
12
+ return
13
+ }
14
+
15
+ node.scale += difference * 0.18
16
+
17
+ node.world.update()
18
+
19
+ node.animation = requestAnimationFrame(animate)
20
+ }
21
+
22
+ node.animation = requestAnimationFrame(animate)
23
+ }
24
+
25
+ export default {
26
+ background: Object.assign(new Image(),
27
+ {src: "data:image/svg+xml," + encodeURIComponent(`
28
+ <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080">
29
+ <defs>
30
+ <linearGradient id="base" x1="0" y1="0" x2="0" y2="1">
31
+ <stop offset="0%" stop-color="#f4f5f7"/>
32
+ <stop offset="100%" stop-color="#e8eaed"/>
33
+ </linearGradient>
34
+
35
+ <radialGradient id="light" cx="50%" cy="42%" r="65%">
36
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
37
+ <stop offset="60%" stop-color="#ffffff" stop-opacity="0.3"/>
38
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
39
+ </radialGradient>
40
+
41
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
42
+ <path
43
+ d="M 40 0 L 0 0 0 40"
44
+ fill="none"
45
+ stroke="#9da3ad"
46
+ stroke-width="1"
47
+ opacity="0.28"
48
+ />
49
+ </pattern>
50
+
51
+ <radialGradient id="fade" cx="50%" cy="50%" r="70%">
52
+ <stop offset="60%" stop-color="#ffffff" stop-opacity="0"/>
53
+ <stop offset="100%" stop-color="#c8ccd3" stop-opacity="0.2"/>
54
+ </radialGradient>
55
+ </defs>
56
+
57
+ <rect width="1920" height="1080" fill="url(#base)"/>
58
+ <rect width="1920" height="1080" fill="url(#light)"/>
59
+ <rect width="1920" height="1080" fill="url(#grid)"/>
60
+ <rect width="1920" height="1080" fill="url(#fade)"/>
61
+ </svg>
62
+ `)}
63
+ ),
64
+
65
+ focusButton: `
66
+ <svg viewBox="0 0 24 24">
67
+ <circle cx="12" cy="12" r="7"/>
68
+ <circle cx="12" cy="12" r="2"/>
69
+ <path d="M12 2V5"/>
70
+ <path d="M12 19V22"/>
71
+ <path d="M2 12H5"/>
72
+ <path d="M19 12H22"/>
73
+ </svg>
74
+ `,
75
+
76
+ rootButton: `
77
+ <svg viewBox="0 0 24 24">
78
+ <circle cx="12" cy="5" r="2.5"/>
79
+ <circle cx="6" cy="19" r="2.5"/>
80
+ <circle cx="18" cy="19" r="2.5"/>
81
+ <path d="M12 7.5V12"/>
82
+ <path d="M12 12L6 16.5"/>
83
+ <path d="M12 12L18 16.5"/>
84
+ </svg>
85
+ `,
86
+
87
+ searchButton: `
88
+ <svg viewBox="0 0 24 24">
89
+ <circle cx="11" cy="11" r="6.5"></circle>
90
+ <line x1="16" y1="16" x2="21" y2="21"></line>
91
+ </svg>
92
+ `,
93
+
94
+ themeButton: `
95
+ <svg
96
+ viewBox="0 0 24 24"
97
+ fill="none"
98
+ stroke="currentColor"
99
+ stroke-width="2"
100
+ stroke-linecap="round"
101
+ stroke-linejoin="round"
102
+ >
103
+ <circle cx="13.5" cy="6.5" r=".5"></circle>
104
+ <circle cx="17.5" cy="10.5" r=".5"></circle>
105
+ <circle cx="8.5" cy="7.5" r=".5"></circle>
106
+ <circle cx="6.5" cy="12.5" r=".5"></circle>
107
+ <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"></path>
108
+ </svg>
109
+ `,
110
+
111
+ draw: {
112
+ line(ctx, object) {
113
+ ctx.save()
114
+
115
+ ctx.lineCap = "round"
116
+
117
+ ctx.shadowColor = "rgba(0, 0, 0, 0.12)"
118
+ ctx.shadowBlur = 4
119
+ ctx.shadowOffsetY = 1
120
+
121
+ ctx.beginPath()
122
+
123
+ ctx.moveTo(
124
+ object.from.x,
125
+ object.from.y
126
+ )
127
+
128
+ ctx.lineTo(
129
+ object.to.x,
130
+ object.to.y
131
+ )
132
+
133
+ ctx.lineWidth = object.width
134
+ ctx.strokeStyle = "#999"
135
+ ctx.stroke()
136
+
137
+ ctx.restore()
138
+ },
139
+
140
+ node(ctx, object) {
141
+ const pos = object.pos
142
+ const size = object.size
143
+
144
+ const width = size.width * object.scale
145
+ const height = size.height * object.scale
146
+
147
+ const x = pos.x - width / 2
148
+ const y = pos.y - height / 2
149
+
150
+ const radius = 10
151
+
152
+ const fake = object.drawNode.layoutNode.original !== null
153
+
154
+ ctx.save()
155
+
156
+ ctx.shadowColor = "rgba(0, 0, 0, 0.14)"
157
+ ctx.shadowBlur = object.hovered ? 16 : 8
158
+ ctx.shadowOffsetY = object.hovered ? 4 : 2
159
+
160
+ ctx.fillStyle = "#f8f8f8"
161
+
162
+ ctx.beginPath()
163
+ ctx.roundRect(x, y, width, height, radius)
164
+ ctx.fill()
165
+
166
+ ctx.shadowColor = "transparent"
167
+
168
+ ctx.save()
169
+
170
+ ctx.beginPath()
171
+ ctx.roundRect(x, y, width, height, radius)
172
+ ctx.clip()
173
+
174
+ ctx.globalAlpha = object.hovered ? 1 : 0.92
175
+
176
+ ctx.drawImage(
177
+ object.image,
178
+ x,
179
+ y,
180
+ width,
181
+ height
182
+ )
183
+
184
+ ctx.restore()
185
+
186
+ ctx.beginPath()
187
+ ctx.roundRect(x, y, width, height, radius)
188
+
189
+ ctx.lineWidth = 1
190
+ ctx.strokeStyle = "rgba(0, 0, 0, 0.08)"
191
+
192
+ if(fake) {
193
+ ctx.setLineDash([4, 4])
194
+ }
195
+
196
+ ctx.stroke()
197
+ ctx.setLineDash([])
198
+
199
+ if(object.root) {
200
+ ctx.beginPath()
201
+ ctx.arc(
202
+ x + width - 9,
203
+ y + 9,
204
+ 3,
205
+ 0,
206
+ Math.PI * 2
207
+ )
208
+
209
+ ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
210
+ ctx.fill()
211
+ }
212
+
213
+ ctx.restore()
214
+ }
215
+ },
216
+
217
+ focus(node) {
218
+ setTimeout(() => {
219
+ node.hovered = true
220
+ animate(node)
221
+
222
+ setTimeout(() => {
223
+ if(node.world.hover !== node) {
224
+ node.hovered = false
225
+ animate(node)
226
+ }
227
+ }, 600)
228
+ }, 150)
229
+ },
230
+
231
+ mouseEnter(node) {
232
+ node.hovered = true
233
+ animate(node)
234
+ },
235
+
236
+ mouseLeave(node) {
237
+ node.hovered = false
238
+ animate(node)
239
+ }
240
+ }
@@ -0,0 +1,150 @@
1
+ import { LayoutGraph, DrawGraph } from "../graph.js"
2
+ import { NodeInstance, Line } from "./objects.js"
3
+
4
+ export default class Bridge {
5
+ constructor(graph, world) {
6
+ this.graph = graph
7
+ this.world = world
8
+
9
+ this.layout = null
10
+ this.draw = null
11
+ this.obj = null
12
+ this.toObj = null
13
+ this.toNode = null
14
+
15
+ this.root = null
16
+ }
17
+
18
+ #cleanWorld() {
19
+ if(this.obj) {
20
+ this.world.bulkRemove(obj => this.obj.includes(obj))
21
+
22
+ this.obj = null
23
+ this.toObj = null
24
+ this.toNode = null
25
+ }
26
+ }
27
+
28
+ calculateLayout(root, recurse, strategy = "First"){ // Real maxdepth, since we cant actually make it infinite
29
+ this.layout = new LayoutGraph(this.graph, root, recurse, strategy)
30
+ // Since JS is goofy like this ill just have recurse be a number when we want recursive
31
+ this.root = this.graph.nodes.get(root)
32
+
33
+ // Invalidate everything else
34
+ this.draw = null
35
+ this.#cleanWorld()
36
+ }
37
+
38
+ calculateDraw(width, height, padX, padY){
39
+ if(!this.layout){
40
+ console.warn("calculateLayout must be called prior to this")
41
+ return
42
+ }
43
+
44
+ this.draw = new DrawGraph(this.layout, width, height, padX, padY)
45
+ this.#cleanWorld()
46
+ }
47
+
48
+ createObjects(maxDepth){
49
+ if(!this.draw){
50
+ console.warn("calculateDraw must be called prior to this")
51
+ return
52
+ }
53
+
54
+ this.#cleanWorld()
55
+
56
+ this.obj = []
57
+ this.toObj = new Map()
58
+ this.toNode = new Map()
59
+
60
+ // Add edges first so nodes render on top
61
+ for (const connection of this.draw.connections) {
62
+ if(
63
+ maxDepth !== null &&
64
+ (
65
+ Math.abs(connection.fromNode.layoutNode.layer) > maxDepth ||
66
+ Math.abs(connection.toNode.layoutNode.layer) > maxDepth
67
+ )
68
+ ){
69
+ continue
70
+ }
71
+
72
+ let obj = new Line(
73
+ this.world,
74
+ connection.from,
75
+ connection.to,
76
+ 2
77
+ )
78
+
79
+ this.obj.push(obj)
80
+ this.world.addObject(obj)
81
+ }
82
+
83
+ // Add nodes
84
+ for (const drawNode of this.draw.nodes) {
85
+ if(
86
+ maxDepth !== null &&
87
+ Math.abs(drawNode.layoutNode.layer) > maxDepth
88
+ ){
89
+ continue
90
+ }
91
+
92
+ let obj = new NodeInstance(
93
+ this.world,
94
+ drawNode.x,
95
+ drawNode.y,
96
+ drawNode.width,
97
+ drawNode.height,
98
+ drawNode.layoutNode.node,
99
+ drawNode.layoutNode.node.val?.icon,
100
+ drawNode
101
+ )
102
+
103
+ obj.root =
104
+ drawNode.layoutNode.node === this.root
105
+
106
+ this.obj.push(obj)
107
+
108
+ if (drawNode.layoutNode.original === null) {
109
+ this.toNode.set(
110
+ obj,
111
+ drawNode.layoutNode.node
112
+ )
113
+
114
+ this.toObj.set(
115
+ drawNode.layoutNode.node,
116
+ obj
117
+ )
118
+ }
119
+
120
+ this.world.addObject(obj)
121
+ }
122
+
123
+ this.focus(this.root)
124
+ }
125
+
126
+ focus(node) {
127
+ if(!this.toObj) {
128
+ console.warn("createObjects must be called prior to this")
129
+ return
130
+ }
131
+
132
+ const obj = this.toObj.get(node)
133
+
134
+ if(!obj) {
135
+ console.warn(`Node ${node.id} is not currently visible`)
136
+ return
137
+ }
138
+
139
+ this.world.camera.pos = [
140
+ obj.pos.x,
141
+ obj.pos.y
142
+ ]
143
+
144
+ this.world.camera.zoom = 1.5
145
+
146
+ obj.focus()
147
+
148
+ this.world.update?.()
149
+ }
150
+ }
@@ -0,0 +1,54 @@
1
+ export default class Camera {
2
+ #world
3
+
4
+ #x = 0
5
+ #y = 0
6
+ #width = 0
7
+ #height = 0
8
+ #zoom = 1
9
+
10
+ constructor(world) {
11
+ this.#world = world
12
+ }
13
+
14
+ set pos(p){
15
+ if (this.#world.paused) return
16
+
17
+ this.#x = p[0]
18
+ this.#y = p[1]
19
+ this.#world.update()
20
+ }
21
+
22
+ set viewport(v){
23
+ if (this.#world.paused) return
24
+
25
+ this.#width = v[0]
26
+ this.#height = v[1]
27
+ this.#world.update()
28
+ }
29
+
30
+ set zoom(val) {
31
+ if (this.#world.paused) return
32
+
33
+ this.#zoom = val
34
+ this.#world.update()
35
+ }
36
+
37
+ get pos(){
38
+ return {
39
+ x: this.#x,
40
+ y: this.#y
41
+ }
42
+ }
43
+
44
+ get viewport(){
45
+ return {
46
+ width: this.#width,
47
+ height: this.#height
48
+ }
49
+ }
50
+
51
+ get zoom(){
52
+ return this.#zoom
53
+ }
54
+ }
@@ -0,0 +1,175 @@
1
+ class Object {
2
+ #world
3
+
4
+ constructor(world) {
5
+ this.#world = world
6
+ }
7
+
8
+ get world() {
9
+ return this.#world
10
+ }
11
+ }
12
+
13
+ class PSInstance extends Object {
14
+ #x
15
+ #y
16
+
17
+ #width
18
+ #height
19
+
20
+ constructor(world, x, y, width, height) {
21
+ super(world)
22
+
23
+ this.#x = x
24
+ this.#y = y
25
+ this.#width = width
26
+ this.#height = height
27
+ }
28
+
29
+ set pos(p) {
30
+ this.#x = p[0]
31
+ this.#y = p[1]
32
+ this.world.update()
33
+ }
34
+
35
+ set size(s) {
36
+ this.#width = s[0]
37
+ this.#height = s[1]
38
+ this.world.update()
39
+ }
40
+
41
+ get pos() {
42
+ return {
43
+ x: this.#x,
44
+ y: this.#y
45
+ }
46
+ }
47
+
48
+ get size() {
49
+ return {
50
+ width: this.#width,
51
+ height: this.#height
52
+ }
53
+ }
54
+ }
55
+
56
+ /*export class test extends PSInstance {
57
+ constructor(world, x, y, width, height, value) {
58
+ super(world, x, y, width, height)
59
+
60
+ this.value = value
61
+ }
62
+
63
+ draw(ctx) {
64
+ let pos = this.pos
65
+ let size = this.size
66
+
67
+ ctx.fillStyle = "red"
68
+ ctx.fillRect(
69
+ pos.x - size.width / 2,
70
+ pos.y - size.height / 2,
71
+ size.width,
72
+ size.height
73
+ )
74
+
75
+ ctx.fillStyle = "white"
76
+ ctx.textAlign = "center"
77
+ ctx.textBaseline = "middle"
78
+ ctx.font = "16px sans-serif"
79
+ ctx.fillText(
80
+ this.value,
81
+ pos.x,
82
+ pos.y
83
+ )
84
+ }
85
+ }*/
86
+
87
+ const placeholder = new Image()
88
+
89
+ placeholder.src = "data:image/svg+xml," + encodeURIComponent(`
90
+ <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
91
+ <text x="100" y="105"
92
+ text-anchor="middle"
93
+ font-family="sans-serif"
94
+ font-size="20"
95
+ fill="#b0b0b5">
96
+ No Image
97
+ </text>
98
+ </svg>
99
+ `)
100
+
101
+ export class ImageInstance extends PSInstance {
102
+ #image
103
+
104
+ constructor(world, x, y, width, height, image) {
105
+ super(world, x, y, width, height)
106
+
107
+ this.image = image
108
+ }
109
+
110
+ set image(img) {
111
+ this.#image = img
112
+ this.world.update()
113
+ }
114
+
115
+ get image() {
116
+ return this.#image ?? placeholder
117
+ }
118
+
119
+ draw(ctx, p, s) {
120
+ let pos = p || this.pos
121
+ let size = s || this.size
122
+
123
+ ctx.drawImage(
124
+ this.image,
125
+ pos.x - size.width / 2,
126
+ pos.y - size.height / 2,
127
+ size.width,
128
+ size.height
129
+ )
130
+ }
131
+ }
132
+
133
+ export class Line extends Object {
134
+ constructor(world, from, to, width = 2) {
135
+ super(world)
136
+
137
+ this.from = from
138
+ this.to = to
139
+ this.width = width
140
+ }
141
+
142
+ draw(ctx) {
143
+ this.world.theme.draw.line(ctx, this)
144
+ }
145
+ }
146
+
147
+ export class NodeInstance extends ImageInstance {
148
+ constructor(world, x, y, width, height, node, image, drawNode) {
149
+ super(world, x, y, width, height, image)
150
+
151
+ this.node = node
152
+ this.hovered = false
153
+ this.scale = 1
154
+ this.animation = null
155
+ this.root = false
156
+
157
+ this.drawNode = drawNode
158
+ }
159
+
160
+ focus() {
161
+ this.world.theme.focus(this)
162
+ }
163
+
164
+ mouseEnter() {
165
+ this.world.theme.mouseEnter(this)
166
+ }
167
+
168
+ mouseLeave() {
169
+ this.world.theme.mouseLeave(this)
170
+ }
171
+
172
+ draw(ctx) {
173
+ this.world.theme.draw.node(ctx, this)
174
+ }
175
+ }
package/world/world.js ADDED
@@ -0,0 +1,240 @@
1
+ export default class World {
2
+ /** @type {HTMLCanvasElement} */
3
+ #canvas
4
+ #ctx
5
+
6
+ #camera
7
+ #theme
8
+
9
+ #paused = false
10
+
11
+ updateRate = 1/60
12
+
13
+ #objects = []
14
+ #background
15
+
16
+ constructor(canvas, theme) {
17
+ this.#theme = theme
18
+ this.#canvas = canvas
19
+ this.#ctx = canvas.getContext("2d")
20
+
21
+ }
22
+
23
+ set paused(value) {
24
+ this.#paused = value
25
+
26
+ if (value) {
27
+ this.#hoveredObject?.mouseLeave?.()
28
+ this.#hoveredObject = null
29
+ }
30
+ }
31
+
32
+ get paused() {
33
+ return this.#paused
34
+ }
35
+
36
+ set theme(value) {
37
+ this.#theme = value
38
+ this.update()
39
+ }
40
+
41
+ get theme() {
42
+ return this.#theme
43
+ }
44
+
45
+ set camera(cam) {
46
+ this.#camera = cam
47
+ this.update()
48
+ }
49
+
50
+ get camera() {
51
+ return this.#camera
52
+ }
53
+
54
+ set background(bg) {
55
+ this.#background = bg
56
+ this.update()
57
+ }
58
+
59
+ get hover() {
60
+ return this.#hoveredObject
61
+ }
62
+
63
+ addObject(obj) {
64
+ if (this.#paused) return
65
+
66
+ let index = this.#objects.findIndex(e => e.z > obj.z)
67
+
68
+ if (index === -1) {
69
+ this.#objects.push(obj)
70
+ } else {
71
+ this.#objects.splice(index, 0, obj)
72
+ }
73
+
74
+ this.update()
75
+ }
76
+
77
+ removeObject(obj) {
78
+ if (this.#paused) return
79
+
80
+ const index = this.#objects.indexOf(obj)
81
+
82
+ if (index !== -1) {
83
+ this.#objects.splice(index, 1)
84
+ }
85
+
86
+ if (this.#hoveredObject === obj) {
87
+ this.#hoveredObject = null
88
+ }
89
+
90
+ this.update()
91
+ }
92
+
93
+ bulkRemove(func) {
94
+ if (this.#paused) return
95
+
96
+ this.#objects = this.#objects.filter(obj => {
97
+ if (func(obj)) {
98
+ if (this.#hoveredObject === obj) {
99
+ this.#hoveredObject = null
100
+ }
101
+ return false
102
+ }
103
+
104
+ return true
105
+ })
106
+
107
+ this.update()
108
+ }
109
+
110
+ clear() {
111
+ if (this.#paused) return
112
+
113
+ this.#objects = []
114
+ this.#hoveredObject = null
115
+
116
+ this.update()
117
+ }
118
+
119
+ get objects() {
120
+ return this.#objects
121
+ }
122
+
123
+ #draw() {
124
+ this.#ctx.clearRect(
125
+ 0,
126
+ 0,
127
+ this.#canvas.width,
128
+ this.#canvas.height
129
+ )
130
+
131
+ if (this.#background) {
132
+ this.#ctx.drawImage(
133
+ this.#background,
134
+ 0,
135
+ 0,
136
+ this.#canvas.width,
137
+ this.#canvas.height
138
+ )
139
+ }
140
+
141
+ this.#ctx.save()
142
+
143
+ const cpos = this.#camera.pos
144
+
145
+ this.#ctx.translate(
146
+ this.#canvas.width / 2,
147
+ this.#canvas.height / 2
148
+ )
149
+
150
+ this.#ctx.scale(
151
+ this.#camera.zoom,
152
+ this.#camera.zoom
153
+ )
154
+
155
+ this.#ctx.translate(
156
+ -cpos.x,
157
+ -cpos.y
158
+ )
159
+
160
+ this.#objects.forEach(e => e.draw(this.#ctx))
161
+
162
+ this.#ctx.restore()
163
+ }
164
+
165
+ #frameRequested = false
166
+
167
+ update() {
168
+ if (this.#paused) return
169
+ if (this.#frameRequested) return
170
+
171
+ this.#frameRequested = true
172
+
173
+ requestAnimationFrame(() => {
174
+ this.#frameRequested = false
175
+
176
+ this.#draw()
177
+ })
178
+ }
179
+
180
+ resize(width, height) {
181
+ this.#canvas.width = width
182
+ this.#canvas.height = height
183
+
184
+ this.#draw()
185
+
186
+ this.#camera.viewport = [width, height]
187
+ }
188
+
189
+ #hoveredObject = null
190
+
191
+ mouseWorldPosition(e) {
192
+ if (!e) {
193
+ return {
194
+ x: null,
195
+ y: null
196
+ }
197
+ }
198
+
199
+ const rect = this.#canvas.getBoundingClientRect()
200
+
201
+ const x =
202
+ e.clientX -
203
+ rect.left -
204
+ this.#canvas.width / 2
205
+
206
+ const y =
207
+ e.clientY -
208
+ rect.top -
209
+ this.#canvas.height / 2
210
+
211
+ return {
212
+ x: x / this.#camera.zoom + this.#camera.pos.x,
213
+ y: y / this.#camera.zoom + this.#camera.pos.y
214
+ }
215
+ }
216
+
217
+ objectAt(x, y) {
218
+ for (let i = this.#objects.length - 1; i >= 0; i--) {
219
+ const object = this.#objects[i]
220
+
221
+ if (!object.pos || !object.size) {
222
+ continue
223
+ }
224
+
225
+ const pos = object.pos
226
+ const size = object.size
227
+
228
+ if (
229
+ x >= pos.x - size.width / 2 &&
230
+ x <= pos.x + size.width / 2 &&
231
+ y >= pos.y - size.height / 2 &&
232
+ y <= pos.y + size.height / 2
233
+ ) {
234
+ return object
235
+ }
236
+ }
237
+
238
+ return null
239
+ }
240
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.