plebeiangraphlibrary 2.2.2 → 2.2.3
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/Examples/14_Interaction_click.html +12 -6
- package/Examples/16_Interaction_details.html +16 -7
- package/Examples/1_ZKC_simple.html +24 -38
- package/Examples/9_Simulation_live.html +24 -16
- package/README.md +269 -0
- package/package.json +1 -1
|
@@ -40,29 +40,35 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
<canvas id="displayCanvas" class="displayCanvas"></canvas>
|
|
42
42
|
<script type="module">
|
|
43
|
-
// Opt-in interaction layer: call enableInteraction() to add click/hover support.
|
|
44
|
-
// Callbacks receive graph details (nodeId, neighbours, position, edge start/end, etc.).
|
|
45
43
|
import * as PGL from "../Build/pgl_module.js";
|
|
46
44
|
|
|
45
|
+
// Interaction is 100% opt-in — nothing is registered until you call enableInteraction().
|
|
46
|
+
// Callbacks receive a details object: { nodeId, data, neighbours, position } for nodes,
|
|
47
|
+
// { edgeId, start, end, data } for edges.
|
|
48
|
+
|
|
47
49
|
const G = await PGL.SampleData.LoadZKCSimulated();
|
|
48
|
-
const width = 800;
|
|
49
|
-
const height = 700;
|
|
50
50
|
const canvas = document.getElementById("displayCanvas");
|
|
51
51
|
const infoText = document.getElementById("infoText");
|
|
52
52
|
|
|
53
|
-
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width, height, canvas });
|
|
53
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
54
54
|
await graph3d.init();
|
|
55
55
|
|
|
56
56
|
const bounds = 1;
|
|
57
|
+
|
|
58
|
+
// Add visual elements BEFORE enableInteraction.
|
|
59
|
+
// The interaction layer works by raycasting against objects already in the scene.
|
|
60
|
+
// If you call enableInteraction first, the raycast has nothing to hit.
|
|
61
|
+
// Thick edges are easier to pick than thin lines — prefer DrawTHREEGraphEdgesThick
|
|
62
|
+
// when you need edge click/hover callbacks.
|
|
57
63
|
const nodeVisualElements = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(G, bounds, 0xffffff, 5);
|
|
58
64
|
const edgeVisualElements = PGL.ThreeWrapper.DrawTHREEGraphEdgesThick(G, bounds, 0xffafcc, 10);
|
|
59
65
|
graph3d.addVisElement(nodeVisualElements);
|
|
60
66
|
graph3d.addVisElement(edgeVisualElements);
|
|
61
67
|
|
|
62
|
-
// Enable opt-in interaction — pass graph and callbacks
|
|
63
68
|
graph3d.enableInteraction({
|
|
64
69
|
graph: G,
|
|
65
70
|
onNodeClick: (d) => {
|
|
71
|
+
// d.neighbours is an array of neighbour node IDs
|
|
66
72
|
infoText.innerHTML = `Node <b>${d.nodeId}</b><br>Neighbours: ${d.neighbours.length}<br>Position: (${d.position?.x?.toFixed(2) ?? "?"}, ${d.position?.y?.toFixed(2) ?? "?"}, ${d.position?.z?.toFixed(2) ?? "?"})`;
|
|
67
73
|
},
|
|
68
74
|
onEdgeClick: (d) => {
|
|
@@ -40,19 +40,20 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
<canvas id="displayCanvas" class="displayCanvas"></canvas>
|
|
42
42
|
<script type="module">
|
|
43
|
-
// Rich interaction: use NodePickDetails to drive visual feedback.
|
|
44
|
-
// ChangeTheVertexColours highlights neighbour nodes by node ID.
|
|
45
43
|
import * as PGL from "../Build/pgl_module.js";
|
|
46
44
|
|
|
45
|
+
// This example combines hover interaction with dynamic vertex coloring.
|
|
46
|
+
// onNodeHover fires with a NodePickDetails object when entering a node,
|
|
47
|
+
// and with null when the cursor leaves — use the null case to reset colors.
|
|
48
|
+
|
|
47
49
|
const G = await PGL.SampleData.LoadZKCSimulated();
|
|
48
|
-
const width = 800;
|
|
49
|
-
const height = 700;
|
|
50
50
|
const canvas = document.getElementById("displayCanvas");
|
|
51
51
|
const infoText = document.getElementById("infoText");
|
|
52
52
|
|
|
53
|
-
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width, height, canvas });
|
|
53
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
54
54
|
await graph3d.init();
|
|
55
55
|
|
|
56
|
+
// Disable camera auto-rotation so hover targets stay stable.
|
|
56
57
|
graph3d.controls.autoRotate = false;
|
|
57
58
|
|
|
58
59
|
const bounds = 1;
|
|
@@ -62,19 +63,27 @@
|
|
|
62
63
|
graph3d.addVisElement(nodeVisualElements);
|
|
63
64
|
graph3d.addVisElement(edgeVisualElements);
|
|
64
65
|
|
|
66
|
+
// IMPORTANT: ChangeTheVertexColours and ResetVertexColors take the inner
|
|
67
|
+
// instanced mesh, NOT the Group returned by the draw call.
|
|
68
|
+
// Always pass nodeVisualElements.children[0] — passing nodeVisualElements
|
|
69
|
+
// directly will silently do nothing.
|
|
70
|
+
const nodeMesh = nodeVisualElements.children[0];
|
|
65
71
|
const HIGHLIGHT_COLOR = 0xff4444;
|
|
66
72
|
|
|
67
73
|
graph3d.enableInteraction({
|
|
68
74
|
graph: G,
|
|
69
75
|
hoverEnabled: true,
|
|
70
76
|
onNodeHover: (d) => {
|
|
71
|
-
|
|
77
|
+
// Reset all node colors before applying a new highlight each frame.
|
|
78
|
+
PGL.ThreeWrapper.ResetVertexColors(nodeMesh);
|
|
72
79
|
if (d) {
|
|
80
|
+
// d.neighbours is the array of neighbour node IDs — color them directly.
|
|
73
81
|
if (d.neighbours.length > 0) {
|
|
74
|
-
PGL.ThreeWrapper.ChangeTheVertexColours(
|
|
82
|
+
PGL.ThreeWrapper.ChangeTheVertexColours(nodeMesh, d.neighbours, HIGHLIGHT_COLOR);
|
|
75
83
|
}
|
|
76
84
|
infoText.innerHTML = `Node <b>${d.nodeId}</b><br>Degree: ${d.neighbours.length}<br>Neighbours highlighted in red`;
|
|
77
85
|
} else {
|
|
86
|
+
// d is null when the pointer leaves a node — colors already reset above.
|
|
78
87
|
infoText.innerHTML = "Hover a node";
|
|
79
88
|
}
|
|
80
89
|
},
|
|
@@ -19,61 +19,47 @@
|
|
|
19
19
|
<div class="canvas-wrap">
|
|
20
20
|
<canvas id="displayCanvas" class="displayCanvas"></canvas>
|
|
21
21
|
<script type="module">
|
|
22
|
-
// import the library
|
|
23
22
|
import * as PGL from "../Build/pgl_module.js";
|
|
24
23
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// STEP 1 — Load the graph.
|
|
25
|
+
// LoadZKCSimulated returns the Zachary Karate Club graph with positions already
|
|
26
|
+
// computed (Kamada-Kawai), so you can skip the layout step entirely.
|
|
27
|
+
// Use LoadZKC() instead if you want to run your own layout.
|
|
27
28
|
const zkcSimulated = await PGL.SampleData.LoadZKCSimulated();
|
|
28
|
-
//
|
|
29
|
-
console.log(zkcSimulated);
|
|
30
|
-
// also as a reference just see how this data is stored
|
|
31
|
-
zkcSimulated.printData();
|
|
29
|
+
zkcSimulated.printData(); // logs node/edge counts to console — useful for debugging
|
|
32
30
|
|
|
33
|
-
//
|
|
34
|
-
const width = 800;
|
|
35
|
-
const heigth = 700;
|
|
36
|
-
|
|
37
|
-
// pass in the graph and the canvas into the drawing object to draw it
|
|
38
|
-
// first get the canvas element
|
|
31
|
+
// STEP 2 — Create the renderer and attach it to the canvas element.
|
|
39
32
|
const canvas = document.getElementById("displayCanvas");
|
|
40
|
-
|
|
41
|
-
// to do that first make a options object
|
|
42
|
-
const graphDrawerOptions = {
|
|
33
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({
|
|
43
34
|
graph: zkcSimulated,
|
|
44
|
-
width:
|
|
45
|
-
height:
|
|
46
|
-
canvas: canvas
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// initialize this object before adding things to it
|
|
35
|
+
width: 800,
|
|
36
|
+
height: 700,
|
|
37
|
+
canvas: canvas,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// STEP 3 — Initialize (async). Sets up the Three.js scene, camera, and
|
|
41
|
+
// WebGL renderer. Must complete before you add any geometry.
|
|
52
42
|
await graph3d.init();
|
|
53
43
|
|
|
54
|
-
// Create
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
44
|
+
// STEP 4 — Create visual geometry and add it to the scene.
|
|
45
|
+
// `bounds` is a global scale factor applied to all positions.
|
|
46
|
+
// DrawTHREEBoxBasedVertices uses box meshes (no texture file required).
|
|
47
|
+
// DrawTHREEGraphVertices uses billboard sprites but needs ./Textures/Square.png.
|
|
48
|
+
const bounds = 1;
|
|
58
49
|
const nodeVisualElements = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(zkcSimulated, bounds, 0xffffff, 5);
|
|
59
|
-
// add the node elements to the scene
|
|
60
50
|
graph3d.addVisElement(nodeVisualElements);
|
|
61
|
-
|
|
51
|
+
|
|
62
52
|
const edgeVisualElements = PGL.ThreeWrapper.DrawTHREEGraphEdgesThick(zkcSimulated, bounds, 0xffafcc, 10);
|
|
63
53
|
graph3d.addVisElement(edgeVisualElements);
|
|
64
54
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// make an animation function
|
|
55
|
+
// STEP 5 — Start the animation loop.
|
|
56
|
+
// graph3d.rendercall() advances OrbitControls (auto-rotate, damping) and
|
|
57
|
+
// renders the frame. You can add your own per-frame logic here too — this is
|
|
58
|
+
// a standard Three.js render loop.
|
|
70
59
|
function animate() {
|
|
71
60
|
requestAnimationFrame(animate);
|
|
72
61
|
graph3d.rendercall();
|
|
73
62
|
}
|
|
74
|
-
|
|
75
|
-
// append the graph renderer to the container
|
|
76
|
-
// and then drawing render calls
|
|
77
63
|
animate();
|
|
78
64
|
</script>
|
|
79
65
|
</div>
|
|
@@ -21,32 +21,36 @@
|
|
|
21
21
|
<script type="module">
|
|
22
22
|
import * as PGL from "../Build/pgl_module.js";
|
|
23
23
|
|
|
24
|
-
// DWT
|
|
24
|
+
// DWT-1005: a 1005-node structural engineering graph. Loaded pre-initialized.
|
|
25
25
|
const G = await PGL.SampleData.LoadDwt1005();
|
|
26
26
|
G.printData();
|
|
27
27
|
|
|
28
|
+
// createKamadaKawai3D returns a simulation object with step(dt) / getPositions().
|
|
29
|
+
// It does NOT run the layout immediately — positions evolve frame-by-frame in the
|
|
30
|
+
// animation loop. This lets you watch the graph "settle" in real time.
|
|
28
31
|
const simulation = PGL.createKamadaKawai3D(G, {
|
|
29
|
-
simulationBound: 800,
|
|
30
|
-
cohesionValue: 1,
|
|
31
|
-
repulsionValue: 1,
|
|
32
|
-
iterationsPerStep: 1,
|
|
32
|
+
simulationBound: 800, // world-space radius the layout tries to fit within
|
|
33
|
+
cohesionValue: 1, // spring attraction strength
|
|
34
|
+
repulsionValue: 1, // node repulsion strength
|
|
35
|
+
iterationsPerStep: 1, // layout steps computed per call to step() — increase for faster convergence
|
|
33
36
|
});
|
|
37
|
+
|
|
38
|
+
// Apply initial positions to the graph before drawing so the draw calls
|
|
39
|
+
// have valid geometry to start from (otherwise nodes all stack at the origin).
|
|
34
40
|
G.apply_position_map(simulation.getPositionMap());
|
|
35
41
|
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
36
42
|
G.apply_edge_pos_maps(lmap);
|
|
37
43
|
|
|
38
|
-
const width = 800;
|
|
39
|
-
const height = 700;
|
|
40
44
|
const canvas = document.getElementById("displayCanvas");
|
|
41
|
-
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({
|
|
42
|
-
graph: G,
|
|
43
|
-
width,
|
|
44
|
-
height,
|
|
45
|
-
canvas,
|
|
46
|
-
});
|
|
45
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
47
46
|
await graph3d.init();
|
|
48
47
|
|
|
49
48
|
const bounds = 0.1;
|
|
49
|
+
|
|
50
|
+
// KEY DECISION — use Mutable variants for any animation.
|
|
51
|
+
// Static variants (DrawTHREEBoxBasedVertices etc.) bake geometry at creation time
|
|
52
|
+
// and cannot be updated. Mutable variants return updatePositions() and updateEdges()
|
|
53
|
+
// functions that push new positions to the GPU each frame without recreating geometry.
|
|
50
54
|
const { group, updatePositions } = PGL.ThreeWrapper.DrawTHREEGraphVerticesMutable(G, bounds, 1, 0xffffff, 1);
|
|
51
55
|
graph3d.addVisElement(group);
|
|
52
56
|
const { group: edgeGroup, updateEdges } = PGL.ThreeWrapper.DrawTHREEGraphEdgesThinMutable(G, bounds, 0xffafcc);
|
|
@@ -56,14 +60,18 @@
|
|
|
56
60
|
function animate() {
|
|
57
61
|
requestAnimationFrame(animate);
|
|
58
62
|
const now = performance.now();
|
|
59
|
-
const dt = (now - lastTime) / 1000;
|
|
63
|
+
const dt = (now - lastTime) / 1000; // delta time in seconds
|
|
60
64
|
lastTime = now;
|
|
65
|
+
|
|
66
|
+
// Advance the layout by one step, then write the new positions back to
|
|
67
|
+
// the graph and push them to the GPU via the mutable updater functions.
|
|
61
68
|
simulation.step(dt);
|
|
62
69
|
G.apply_position_map(simulation.getPositionMap());
|
|
63
70
|
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
64
71
|
G.apply_edge_pos_maps(lmap);
|
|
65
|
-
updatePositions(simulation.getPositions());
|
|
66
|
-
updateEdges();
|
|
72
|
+
updatePositions(simulation.getPositions()); // Float32Array [x0,y0,z0, x1,y1,z1, ...]
|
|
73
|
+
updateEdges(); // recomputes edge line geometry from node positions
|
|
74
|
+
|
|
67
75
|
graph3d.rendercall();
|
|
68
76
|
}
|
|
69
77
|
animate();
|
package/README.md
CHANGED
|
@@ -14,6 +14,51 @@ It can be a bit confusing especially when working with Nodes/Edges/Vertices/Line
|
|
|
14
14
|
Lastly, there are a few helper classes like points and lines. Points are essentially vectors and are used for displacement and also for describing a place in relation to the global coordinate system. Line are an array of points that get translated into lines using one of the visualization methods. Points can have different visualizations like boxes, billboarded planes and cylinders etc.
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
## Quick Start: The 5-Step Pipeline
|
|
18
|
+
|
|
19
|
+
Every PGL visualization follows the same five steps **in this order**. Getting the order wrong is the most common source of silent bugs.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Step 1 — Load or build a Graph
|
|
23
|
+
PGL.SampleData.LoadZKCSimulated() ← positions already baked in (fastest start)
|
|
24
|
+
PGL.SampleData.LoadZKC() ← raw graph, you must run a layout first
|
|
25
|
+
GenerateErdosReyni_n_p(n, p) + await G.initialize() ← manual build
|
|
26
|
+
|
|
27
|
+
Step 2 — Create the renderer
|
|
28
|
+
new PGL.GraphDrawer.GraphDrawer3d({ graph, canvas, width, height })
|
|
29
|
+
|
|
30
|
+
Step 3 — Initialize (async — must complete before anything else)
|
|
31
|
+
await graph3d.init()
|
|
32
|
+
|
|
33
|
+
Step 4 — Draw visual elements and add them to the scene
|
|
34
|
+
const nodes = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(G, bounds, 0xffffff, 5)
|
|
35
|
+
graph3d.addVisElement(nodes)
|
|
36
|
+
const edges = PGL.ThreeWrapper.DrawTHREEGraphEdgesThick(G, bounds, 0xffafcc, 10)
|
|
37
|
+
graph3d.addVisElement(edges)
|
|
38
|
+
// If you need interaction, call enableInteraction() here — after addVisElement
|
|
39
|
+
graph3d.enableInteraction({ graph: G, onNodeClick: (d) => console.log(d) })
|
|
40
|
+
|
|
41
|
+
Step 5 — Start the animation loop
|
|
42
|
+
function animate() { requestAnimationFrame(animate); graph3d.rendercall(); }
|
|
43
|
+
animate()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Static vs. Mutable — choose before drawing:**
|
|
47
|
+
|
|
48
|
+
| Scenario | Use these draw calls | Returns |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Fixed layout (no animation needed) | `DrawTHREEBoxBasedVertices`, `DrawTHREEGraphEdgesThick` | `THREE.Group` — pass directly to `addVisElement` |
|
|
51
|
+
| Live simulation, drag-to-reposition | `DrawTHREEBoxBasedVerticesMutable`, `DrawTHREEGraphEdgesThinMutable` | `{ group, updatePositions(), updateEdges() }` — call updaters each frame |
|
|
52
|
+
|
|
53
|
+
Mutable variants return an `updatePositions(posArray)` and `updateEdges()` function. Call both each frame *before* `rendercall()`.
|
|
54
|
+
|
|
55
|
+
**When do I need to call `await G.initialize()`?**
|
|
56
|
+
|
|
57
|
+
- `LoadZKCSimulated()`, `LoadZKC()`, `LoadDwt1005()`, `Graph.create()` → **auto-initialized, skip this step**
|
|
58
|
+
- `GenerateErdosReyni_n_p()` or building a graph manually with `add_node` / `add_edge` → **you must call `await G.initialize()` yourself** before drawing or running algorithms
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
17
62
|
## Documentation
|
|
18
63
|
|
|
19
64
|
The documentation for the package is available at [documentation](https://www.plebeiangraphlibrary.com/). You can also generate API docs locally with:
|
|
@@ -180,6 +225,230 @@ async function createVisualization() {
|
|
|
180
225
|
createVisualization();
|
|
181
226
|
```
|
|
182
227
|
|
|
228
|
+
## Cookbook: Copy-Paste Recipes
|
|
229
|
+
|
|
230
|
+
Each recipe below is self-contained and runnable. They assume you have imported the library as `import * as PGL from "plebeiangraphlibrary"` and have a `<canvas id="displayCanvas">` element in the page.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### Recipe 1 — Load your own graph from a plain edge list
|
|
235
|
+
|
|
236
|
+
PGL supports the same space-separated edge list format used by [(sgd)²](https://github.com/jxz12/s_gd2). Each line is `nodeA nodeB`, one edge per line.
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
const edgeListText = `
|
|
240
|
+
0 1
|
|
241
|
+
0 2
|
|
242
|
+
1 2
|
|
243
|
+
2 3
|
|
244
|
+
3 4
|
|
245
|
+
`.trim();
|
|
246
|
+
|
|
247
|
+
const G = await PGL.SampleData.LoadGraphFromEdgeListText(edgeListText);
|
|
248
|
+
// G is already initialized and has random starting positions.
|
|
249
|
+
// Run a layout before drawing (positions are random without it):
|
|
250
|
+
const posMap = PGL.Drawing.SimulateKamadaKawai(G, 50, 200);
|
|
251
|
+
G.apply_position_map(posMap);
|
|
252
|
+
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
253
|
+
G.apply_edge_pos_maps(lmap);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Recipe 2 — Build a graph programmatically and run a layout
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
import * as PGL from "plebeiangraphlibrary";
|
|
262
|
+
|
|
263
|
+
const G = new PGL.Graph();
|
|
264
|
+
|
|
265
|
+
// add_node(id, { pos: PointLike, ...yourData })
|
|
266
|
+
G.add_node(0, { pos: { x: 0, y: 0, z: 0 } });
|
|
267
|
+
G.add_node(1, { pos: { x: 0, y: 0, z: 0 } });
|
|
268
|
+
G.add_node(2, { pos: { x: 0, y: 0, z: 0 } });
|
|
269
|
+
G.add_edge(0, 1);
|
|
270
|
+
G.add_edge(1, 2);
|
|
271
|
+
G.add_edge(2, 0);
|
|
272
|
+
|
|
273
|
+
// Manual builds require an explicit initialize() call — loaders handle this automatically
|
|
274
|
+
await G.initialize();
|
|
275
|
+
|
|
276
|
+
// Now run a layout to compute real positions
|
|
277
|
+
const posMap = PGL.Drawing.SimulateKamadaKawai(G, 100, 200);
|
|
278
|
+
G.apply_position_map(posMap);
|
|
279
|
+
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
280
|
+
G.apply_edge_pos_maps(lmap);
|
|
281
|
+
|
|
282
|
+
// Then draw normally (steps 2–5 of the pipeline)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Recipe 3 — Color nodes by a data property
|
|
288
|
+
|
|
289
|
+
Nodes are drawn as an instanced mesh. `ChangeTheVertexColours` takes the **inner mesh** (`.children[0]`), not the Group.
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
const G = await PGL.SampleData.LoadZKCSimulated();
|
|
293
|
+
// ... (pipeline steps 2–4) ...
|
|
294
|
+
|
|
295
|
+
const nodeGroup = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(G, 1, 0xffffff, 5);
|
|
296
|
+
graph3d.addVisElement(nodeGroup);
|
|
297
|
+
|
|
298
|
+
// Color specific nodes red — pass the inner mesh, not the Group
|
|
299
|
+
const mesh = nodeGroup.children[0];
|
|
300
|
+
const redNodeIds = [0, 3, 7, 12];
|
|
301
|
+
PGL.ThreeWrapper.ChangeTheVertexColours(mesh, redNodeIds, 0xff4444);
|
|
302
|
+
|
|
303
|
+
// To reset all colors back to white:
|
|
304
|
+
// PGL.ThreeWrapper.ResetVertexColors(mesh);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
To color nodes by a data property (e.g. a "community" field you stored in `node.data`):
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
const communityColors = { 0: 0xff6b6b, 1: 0x4ecdc4, 2: 0xffe66d, 3: 0xa8e6cf };
|
|
311
|
+
for (const [community, color] of Object.entries(communityColors)) {
|
|
312
|
+
const ids = [...G.nodes.entries()]
|
|
313
|
+
.filter(([, n]) => n.data?.community === Number(community))
|
|
314
|
+
.map(([id]) => id);
|
|
315
|
+
PGL.ThreeWrapper.ChangeTheVertexColours(mesh, ids, color);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### Recipe 4 — Tooltip on hover (HTML overlay)
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
const G = await PGL.SampleData.LoadZKCSimulated();
|
|
325
|
+
const canvas = document.getElementById("displayCanvas");
|
|
326
|
+
|
|
327
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
328
|
+
await graph3d.init();
|
|
329
|
+
|
|
330
|
+
graph3d.controls.autoRotate = false; // disable rotation so hover is stable
|
|
331
|
+
|
|
332
|
+
const nodeGroup = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(G, 1, 0xffffff, 5);
|
|
333
|
+
graph3d.addVisElement(nodeGroup);
|
|
334
|
+
graph3d.addVisElement(PGL.ThreeWrapper.DrawTHREEGraphEdgesThick(G, 1, 0xffafcc, 10));
|
|
335
|
+
|
|
336
|
+
// Create a tooltip div in your HTML: <div id="tooltip" style="position:absolute;display:none;..."></div>
|
|
337
|
+
const tooltip = document.getElementById("tooltip");
|
|
338
|
+
|
|
339
|
+
// enableInteraction must come after addVisElement
|
|
340
|
+
graph3d.enableInteraction({
|
|
341
|
+
graph: G,
|
|
342
|
+
onNodeHover: (d) => {
|
|
343
|
+
if (d) {
|
|
344
|
+
tooltip.style.display = "block";
|
|
345
|
+
tooltip.textContent = `Node ${d.nodeId} — ${d.neighbours.length} neighbours`;
|
|
346
|
+
} else {
|
|
347
|
+
tooltip.style.display = "none";
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
function animate() { requestAnimationFrame(animate); graph3d.rendercall(); }
|
|
353
|
+
animate();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
### Recipe 5 — Highlight a node's neighbours on click
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
const G = await PGL.SampleData.LoadZKCSimulated();
|
|
362
|
+
const canvas = document.getElementById("displayCanvas");
|
|
363
|
+
|
|
364
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
365
|
+
await graph3d.init();
|
|
366
|
+
|
|
367
|
+
const nodeGroup = PGL.ThreeWrapper.DrawTHREEBoxBasedVertices(G, 1, 0xffffff, 5);
|
|
368
|
+
graph3d.addVisElement(nodeGroup);
|
|
369
|
+
graph3d.addVisElement(PGL.ThreeWrapper.DrawTHREEGraphEdgesThick(G, 1, 0x555555, 10));
|
|
370
|
+
|
|
371
|
+
const mesh = nodeGroup.children[0]; // inner instanced mesh
|
|
372
|
+
|
|
373
|
+
graph3d.enableInteraction({
|
|
374
|
+
graph: G,
|
|
375
|
+
onNodeClick: (d) => {
|
|
376
|
+
PGL.ThreeWrapper.ResetVertexColors(mesh); // clear previous highlight
|
|
377
|
+
PGL.ThreeWrapper.ChangeTheVertexColours(mesh, [d.nodeId], 0xffdd00); // clicked node = yellow
|
|
378
|
+
PGL.ThreeWrapper.ChangeTheVertexColours(mesh, d.neighbours, 0xff4444); // neighbours = red
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
function animate() { requestAnimationFrame(animate); graph3d.rendercall(); }
|
|
383
|
+
animate();
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### Recipe 6 — Live layout simulation (Kamada–Kawai)
|
|
389
|
+
|
|
390
|
+
Use `createKamadaKawai3D` + mutable draw calls so the geometry updates each frame without recreating it.
|
|
391
|
+
|
|
392
|
+
```javascript
|
|
393
|
+
const G = await PGL.SampleData.LoadDwt1005();
|
|
394
|
+
|
|
395
|
+
const simulation = PGL.createKamadaKawai3D(G, {
|
|
396
|
+
simulationBound: 500,
|
|
397
|
+
cohesionValue: 1,
|
|
398
|
+
repulsionValue: 1,
|
|
399
|
+
iterationsPerStep: 1,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Apply initial positions so the draw calls have something to start from
|
|
403
|
+
G.apply_position_map(simulation.getPositionMap());
|
|
404
|
+
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
405
|
+
G.apply_edge_pos_maps(lmap);
|
|
406
|
+
|
|
407
|
+
const canvas = document.getElementById("displayCanvas");
|
|
408
|
+
const graph3d = new PGL.GraphDrawer.GraphDrawer3d({ graph: G, width: 800, height: 700, canvas });
|
|
409
|
+
await graph3d.init();
|
|
410
|
+
|
|
411
|
+
// Mutable variants return updater functions — required for animation
|
|
412
|
+
const { group, updatePositions } = PGL.ThreeWrapper.DrawTHREEGraphVerticesMutable(G, 0.1, 1, 0xffffff, 1);
|
|
413
|
+
const { group: edgeGroup, updateEdges } = PGL.ThreeWrapper.DrawTHREEGraphEdgesThinMutable(G, 0.1, 0xffafcc);
|
|
414
|
+
graph3d.addVisElement(group);
|
|
415
|
+
graph3d.addVisElement(edgeGroup);
|
|
416
|
+
|
|
417
|
+
let lastTime = performance.now();
|
|
418
|
+
function animate() {
|
|
419
|
+
requestAnimationFrame(animate);
|
|
420
|
+
const now = performance.now();
|
|
421
|
+
simulation.step((now - lastTime) / 1000); // advance the layout
|
|
422
|
+
lastTime = now;
|
|
423
|
+
|
|
424
|
+
G.apply_position_map(simulation.getPositionMap());
|
|
425
|
+
const lmap = PGL.Drawing.DrawEdgeLinesDivisions(G, 1);
|
|
426
|
+
G.apply_edge_pos_maps(lmap);
|
|
427
|
+
updatePositions(simulation.getPositions()); // push new positions to GPU
|
|
428
|
+
updateEdges(); // recompute edge geometry
|
|
429
|
+
graph3d.rendercall();
|
|
430
|
+
}
|
|
431
|
+
animate();
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Common Mistakes
|
|
437
|
+
|
|
438
|
+
These are the issues that trip up most new users and AI-generated code. Check this list first when something doesn't render or a callback never fires.
|
|
439
|
+
|
|
440
|
+
| Mistake | Symptom | Fix |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| Passing the `Group` to `ChangeTheVertexColours` | Colors don't change | Pass `nodeVisualElements.children[0]` — the function needs the inner instanced mesh, not the outer Group. Same for `ResetVertexColors`. |
|
|
443
|
+
| Forgetting `await G.initialize()` after manually building a graph | BFS, Dijkstra, and layout return empty/wrong results | `GenerateErdosReyni_n_p` and manual `add_node` / `add_edge` loops do **not** auto-initialize. Always call `await G.initialize()` before running any algorithm or draw call. |
|
|
444
|
+
| Calling `initialize()` without `await` | Race condition — adjacency lists are half-built | It is async. Always `await G.initialize()`. |
|
|
445
|
+
| Using `DrawTHREEGraphVertices` in a bundler project (Vite, Parcel, Webpack) | Nodes invisible — texture `./Textures/Square.png` fails to load silently | Copy `Examples/Textures/` to your project's public folder, or use `DrawTHREEBoxBasedVertices` which needs no texture. |
|
|
446
|
+
| Calling `enableInteraction()` before `addVisElement()` | Callbacks never fire | The interaction layer raycasts against objects already in the scene. Always add all visual elements first, then call `enableInteraction()`. |
|
|
447
|
+
| Expecting `Dijkstra` to use edge weights | Shortest path looks wrong on weighted graphs | `Dijkstra` in PGL returns **hop counts** (unweighted BFS). There is no weighted shortest-path solver — use hop counts or implement your own on top of `get_adjacency()`. |
|
|
448
|
+
| Using a static draw call then trying to update positions | Node positions don't move in the animation loop | Static variants (`DrawTHREEBoxBasedVertices`, `DrawTHREEGraphEdgesThick`) bake geometry at creation. For live updates use the `Mutable` variants and call `updatePositions()` / `updateEdges()` each frame. |
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
183
452
|
## Testing
|
|
184
453
|
|
|
185
454
|
- **Unit tests:** `npm run test:unit` (Vitest; tests Utilities, Graph, Simulation, matrix helpers).
|