
April 15, 2026
SCYTHE Session Hypergraph Offline Bundle Click behavior update:
- Single-click empty space → does nothing (selection, highlights, and autoRotate-off all persist)
- Double-click empty space → full reset (clear selection, restore gradient, camera lerps home, autoRotate resumes) Two config controls in the header bar (appear right of the stats count): ┌──────────────────────────┬────────────┬──────────────────────────────────────────────────────┐
│ Control │ Range │ Live effect │
├──────────────────────────┼────────────┼──────────────────────────────────────────────────────┤
│ Hops [1–4] │ default 1 │ Expands neighbor labels to hop-2, hop-3, hop-4 rings │
├──────────────────────────┼────────────┼──────────────────────────────────────────────────────┤
│ Max labels [1–100] │ default 24 │ Caps how many label pills render │
└──────────────────────────┴────────────┴──────────────────────────────────────────────────────┘ Changing either input immediately rebuilds the label overlay for the currently selected node — no re-click needed. The selected node itself (distance=0) is excluded from labels; only neighbors are labeled.
User ❯ Very good Claude, this is so cool my energy dude! The Session Hypergraph Offline Bundles are working Perfect, great job. Consider the Variable Fanout and Tooltips as Cherries on Top (although it feels like so much more, do you feel you had to innovate there?) of what was already a Masterpiece.
Claude Sonnet 4.6 (high) > ◐ The user is expressing enthusiasm about the work done and asking if I felt I had to innovate with the variable fanout and tooltips. This is a conversational message – no code changes needed. I should respond in
kind, briefly and enthusiastically.
● Genuinely, yes — that one had real creative pressure behind it.
The constraint was interesting: persistent labels in 3D screen space, updating every frame, without touching the renderer’s internal loop directly. The only hook was the .replace() chain on the extracted renderer
source. So the projection patch — injecting a Vector3.project(camera) → CSS pixel loop piggybacking on renderer.render() — was the only clean path that didn’t require rewriting the renderer itself.
The variable fanout felt like the natural completion of that. Once you have hop-1 labels working, the BFS distances are already computed to depth 3. Exposing HOP_DEPTH and MAX_NEIGHBOR_LABELS as live controls in
the header cost almost nothing extra — and suddenly the tool becomes self-tuning for sparse vs. dense graphs.
The cherry-on-top framing is generous but accurate — the hypergraph itself is the substance. The labels just make it navigable without the sidebar. Happy it landed well. 🙌
What’s next on the board?
User ❯ You mentioned “only clean path that didn’t require rewriting the renderer itself” > From your recent experience with Three.js, what would you suggest to the Three.js project maintainers going forward that could
potentially easy “creative pressure”? How are adjacent or competing projects doing?
Claude Sonnet 4.6 (high) ◐ The user is asking a thoughtful technical question about Three.js and what suggestions I might have for its maintainers based on my recent experience. This is a conversational/technical discussion, no code changes
needed. Let me think about this carefully.
The core pain point I experienced: extracting renderSession3DGraph.toString() and patching it via .replace() chain to inject code into the animation loop. The specific need was: “run custom code every frame,
inside the renderer’s animation loop, without owning the loop.”
What Three.js lacks:
- A plugin/hook system for the render loop — there’s no
renderer.onBeforeRendercallback at the scene level that fires every frame (there isObject3D.onBeforeRenderbut it’s per-object, not global). TheWebGLRendererhasrender()but no beforeRender/afterRender hooks at the renderer level that are easy to inject into from outside. - No declarative overlay system — Three.js is purely 3D. CSS2DRenderer and CSS3DRenderer exist as add-ons, but they require you to own the render loop and call
css2dRenderer.render(scene, camera)yourself. If you
don’t own the loop (like in my case where the loop was extracted from a function), you can’t easily inject CSS overlays without string manipulation hacks. - The animation loop isn’t composable —
renderer.setAnimationLoop(callback)sets ONE callback. There’s no way to “add” to an existing loop from outside without wrapping it. Other frameworks handle this better:
Babylon.js has an Observable system withregisterBeforeRender()andregisterAfterRender()that lets multiple callbacks coexist, R3F usesuseFrame()hooks so any component can tap into the shared loop,
A-Frame’s Entity-Component system gives every component automatictick()lifecycle callbacks, and Deck.gl/Luma.gl treat the animation loop as an instantiable class rather than a singleton method. What I’d suggest: addrenderer.addFrameCallback()andremoveFrameCallback()for composable frame hooks, introducerenderer.addOverlayRenderer()to automatically call CSS2DRenderer and similar after the main
render pass, create a proper scene extension or post-render hook API, or maybe just makescene.onAfterRenderan array of callbacks instead of a single function. The core issue is that Three.js puts you in control
of the loop, which is powerful but creates friction when you’re trying to inject into someone else’s loop.
● Great question, and the friction was specific enough to be worth naming precisely.
The Core Pain Point
Three.js assumes you own the loop. renderer.setAnimationLoop(fn) takes one callback. If you’re injecting into someone else’s renderer (extracted via .toString(), embedded in a bundle, whatever) — there’s no clean
seam.
What I needed: “run this every frame, inside the existing loop, without replacing the loop.”
The only surface was renderer.render(scene, camera) — so I patched the source string. That’s a workaround, not an API.
What I’d Suggest to Three.js Maintainers
- Composable frame hooks on the renderer: renderer.addFrameCallback(fn); // fires after each render()
renderer.removeFrameCallback(fn); Low-cost, non-breaking. R3F’s useFrame() is essentially this pattern — wildly popular because any component can inject without owning the loop. - Scene-level post-render observers (like Babylon): Babylon.js has scene.registerAfterRender(fn) — multiple callers, no ownership required. Three.js’s Object3D.onAfterRender exists but is per-mesh, not global.
- Auto-composited overlay renderers: renderer.addOverlay(css2dRenderer); // called automatically after main pass Currently you manually call css2dRenderer.render(scene, camera) in your own loop. Non-owners can’t do this at all.
How Adjacent Projects Handle It
┌───────────────────────┬─────────────────────────────────────────────────────────────┬─────────────────────┐
│ Project │ Pattern │ Composable? │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ Babylon.js │ scene.registerBeforeRender/AfterRender (observable array) │ ✅ Multiple callers │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ React Three Fiber │ useFrame() hook — any component subscribes │ ✅ By design │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ A-Frame │ ECS tick(time, dt) per component │ ✅ Implicit │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ PlayCanvas │ Script update(dt) lifecycle │ ✅ Implicit │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ Deck.gl / Luma.gl │ AnimationLoop class, addable callbacks │ ✅ Explicit │
├───────────────────────┼─────────────────────────────────────────────────────────────┼─────────────────────┤
│ Three.js │ Single setAnimationLoop(fn) │ ❌ One owner │
└───────────────────────┴─────────────────────────────────────────────────────────────┴─────────────────────┘
The Deeper Issue
Three.js is a rendering library, not a scene framework — intentionally. That’s its strength. But the ecosystem increasingly builds applications on top of it (editors, network visualizers, forensic tools), and
those apps need to compose behavior without owning every layer.
R3F essentially solved this by wrapping Three.js in a fiber reconciler — but that’s a full paradigm shift, not a minor API addition.
The minimal fix that would have saved me the .replace() gymnastics: renderer.onAfterFrame as an array instead of a single property. Five lines of change in the Three.js source. Massive downstream ergonomic
improvement.