agentsclimarketplace

Vectojs graph3d

Skill vectojs/vectojs-skills/skills/vectojs-graph3d

Agent skills for building, optimizing, embedding, and exporting VectoJS projects with Codex, Claude Code, Cursor, and GitHub Copilot.

Install
npx -y skills add vectojs/vectojs-skills --skill vectojs-graph3d

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when building or debugging a 3D force-directed graph with @vectojs/graph3d — Graph3D instanced rendering, the GraphLayout contract, VectoForceLayout (in-house Barnes-Hut) vs D3ForceLayout, GraphInteraction hover/select/drag-to-pin, or when a graph layout is slow, unstable, or non-deterministic.

SKILL.md

6.0 KB, as published. Nobody here has run it

VectoJS Graph3D

@vectojs/graph3d renders a force-directed graph as instanced Three.js geometry and keeps layout strictly separate from rendering. Requires @vectojs/three and three alongside it.

Architecture: layout and renderer are decoupled

The renderer is deliberately ignorant of how positions were produced.

import { Graph3D, VectoForceLayout, GraphInteraction } from "@vectojs/graph3d";

const graph = new Graph3D({ nodeRadius: 4 });
graph.setGraphData({ nodes, links }); // rebuilds instanced buffers

const layout = new VectoForceLayout();
layout.setGraph({ nodes, links });

function frame() {
  const settled = layout.step(); // advance the simulation
  graph.applyPositions(layout.positions); // xyz triplets in node order
  if (!settled) requestAnimationFrame(frame);
}
  • setGraphData() rebuilds GPU resources — instanced buffers are fixed-size, so a changed node/link count means fresh meshes. Styling-only changes to the same topology don't need it.
  • applyPositions(Float32Array) takes xyz triplets in node order. Call it after every layout step that moved something.
  • Unknown link endpoints throw rather than silently drawing a line to the origin — a wrong id is a bug, not a visual glitch.

Choosing a layout

Both implement the same GraphLayout contract (setGraph, step(iterations?) returning "settled", positions, and optional pinNode/unpinNode/reheat), so they are drop-in swappable.

VectoForceLayoutD3ForceLayout
Dependenciesnone (in-house)d3-force-3d
AlgorithmBarnes-Hut octree N-body, O(N log N)/tickd3's force simulation
Determinismseeded PRNG, f32 throughoutdepends on d3
Measured4.2–7.2× faster (Chrome), 5.0–8.3× (Firefox) per tick at 500–5000 nodes; margin widens with Nbaseline

Default to VectoForceLayout. It removes a dependency and is several times faster; D3ForceLayout remains for parity with an existing d3 tuning.

Tuning VectoForceLayout

Defaults are chosen so linked nodes settle closer than unlinked ones:

  • linkDistance (30) — spring resting length.
  • linkStrength (0.3) — fraction of overshoot corrected per tick, scaled by alpha.
  • repulsion (300) — positive magnitude (d3 expresses this as negative charge).
  • centerStrength (0.02) — pull toward the origin.
  • velocityDecay (0.6) — per-tick velocity retention, i.e. 1 - friction.
  • theta (0.9) — Barnes-Hut opening angle. 0 = exact O(N²); larger = faster and looser. Raise it before lowering node count.
  • alphaDecay (0.0228) — d3's default, ~300 ticks to cool.

step() returns true once cooled. Call reheat() after a topology or pin change instead of rebuilding the layout.

Interaction

GraphInteraction wires hover, select, and drag-to-pin against the renderer's pickNode(raycaster). Drag-to-pin routes through pinNode/unpinNode, which is why those are part of the layout contract — a pinned node is held by the simulation, not by the renderer.

graph.getNodePosition(index, target) reads a node's current world position into a THREE.Vector3 you own (returns null for an out-of-range index).

Common mistakes

  • Rebuilding Graph3D every frame. setGraphData() is a GPU rebuild; only call it when the node/link count changes.
  • Stepping the layout inside render(). Step it in your frame loop, then hand positions to the renderer. Mixing them makes the simulation frame-rate dependent.
  • Not calling dispose(). Both Graph3D and GraphInteraction own GPU resources and listeners.
  • Assuming positions is a copy. It's the live buffer; copy it if you need a snapshot.
  • Reaching for a WASM kernel. Deliberately not built — see below.

Performance notes (measured)

  • applyPositions derives the instanced mesh's bounding sphere inline from the positions it already has, rather than calling InstancedMesh.computeBoundingSphere() (which re-reads every instance matrix — it measured at 60–78% of the whole method). Frustum culling stays correct because the sphere expands by each instance's true world radius (nodeRadius × cbrt(val)). Net 2.3–3.2× faster.
  • linkLines sets frustumCulled = false (a line set spanning the whole graph is never meaningfully cullable); nodeMesh keeps culling on.
  • A Rust/WASM force kernel is deliberately deferred. The JS Barnes-Hut is already 4–8× over d3; a kernel would need either a bad dependency direction (graph3d → heavy @vectojs/core, just to load a wasm URL) or a whole new crate + CI wiring, and the per-frame octree is data-dependent, so bit-identical cross-engine differential testing is materially harder than for the transform/particle kernels. Don't start one without a measurement showing the JS layout is the bottleneck.

Verification

  • Layout is deterministic: same input + same seed ⇒ same positions. Assert that rather than a screenshot.
  • step() eventually returns true; a layout that never settles is a tuning bug (usually velocityDecay too high or repulsion fighting centerStrength).
  • For frame-time claims use the real-browser harness (see the hyprland-browser-bench skill) and quote both engines — V8 and SpiderMonkey diverge noticeably on this workload.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.