feat(web): Document Explorer adopts the unified engine (ADR-702 phase 4)#368
Open
aaronsb wants to merge 8 commits into
Open
feat(web): Document Explorer adopts the unified engine (ADR-702 phase 4)#368aaronsb wants to merge 8 commits into
aaronsb wants to merge 8 commits into
Conversation
Extends the engine's `<Nodes>` and `<Scene>` to support per-node geometry classes. When the plugin passes `nodeClasses` (a class key per node) and `geometryByClass` (a JSX geometry per class), the engine partitions nodes by class and renders one InstancedMesh per class. Pointer events are mapped class-local → global node id via per-class index maps. Also adds `nodeScales` (optional per-node base scale, replaces the degree-based default when present). Document Explorer documents need a large constant scale; concepts use the engine default — this prop lets the plugin opt out of the degree formula without forking the math. All three props are optional and backward-compatible. When omitted (Force Graph today), behaviour is identical to the previous single InstancedMesh + icosahedron + degree-based scales — same draw count, same pointer event path. Verified by typecheck and the unit suite. This is the engine surface the Document Explorer port (next commit) needs to render documents-as-glyphs alongside concepts-as-dots.
…e 4) Replaces the 604-line d3+SVG implementation with a ~340-line thin wrapper around the engine's `<Scene>`. Document Explorer keeps its visual experience — amber document glyphs, query-concept dots, extended-concept dots, focus dimming, info legend, NodeInfoBox — but inherits physics, drag, hover/select, projection (2D/3D), and labels from the engine. Engine surface used: - `nodeClasses` + `geometryByClass` distinguish documents (boxGeometry) from concepts (icosahedronGeometry) — one InstancedMesh per class. - `nodeScales` gives documents a large constant scale relative to concepts; the engine no longer needs to know which is which. - `activeIds` + `dimAlpha` drive focus-mode dimming. - `simHandleRef` exposes reheat to the overlay button. - `projection` setting toggles 2D/3D via the engine's camera dispatch. What carries over unchanged: `DocumentExplorerWorkspace` keeps its direct-mount pattern (workspace builds data, passes `focusedDocumentId`, `onFocusChange`, `onViewDocument` as extra props). Settings shape adds `projection` but keeps all existing fields. Click model: - Single-click on document → toggle focus - Double-click on document → onViewDocument (manual dblclick detection in the plugin since the engine fires one onSelect per click) - Single-click on concept → toggle NodeInfoBox - Background click → clear selection (engine default) Deferred (carried as props through the component but not yet rendered; follow-up issues will land them): - Passage rings (concentric arcs around concept hits) - NodeInfoBox positioned over the node (currently corner-pinned) - Per-edge visibility for document→concept clustering hints - Type-specific force tuning (engine uses unified defaults) Force Graph behaviour is unaffected — it doesn't pass `nodeClasses`, `geometryByClass`, or `nodeScales`, so the engine renders the single icosahedron mesh exactly as before.
…o colors Two issues the advisor flagged from a review of the port: 1. **Concepts drift from their documents**: the d3 implementation relied on invisible document→concept links to pull concept dots toward their parent document (concepts of one doc rarely link directly to each other in the data). The first cut of the port dropped those links, which left clustering to the engine's center gravity alone — not enough to keep documents and their concepts visibly grouped. Engine fix: `<Edges>` and `<Scene>` accept `edgeVisible?: boolean[]` parallel to `edges`. When false, the edge stays in the physics sim (which consumes the same `edges` array) but renders collapsed to a point. Plugin fix: Document Explorer now passes ALL links to the engine, marking `l.visible === false` clustering hints with `edgeVisible[i] = false`. 2. **Focus dim didn't dim node meshes**: engine `<Nodes>` doesn't read `activeIds`/`dimAlpha` (only `<Edges>` and labels do). With the first-cut port, focus mode dimmed edges and labels but left documents and concepts at full brightness — visible regression from the d3 version. Fix: Document Explorer now bakes the dim multiplier into the per-node color array, matching the pattern Force Graph uses for hover focus. Also: honest comment on `physicsActive` setTimeout — the engine doesn't expose a settle-end callback yet, so the spinner is a fixed-duration visual hint, not a real signal. `frameloop="demand"` is fine — both engine `<Edges>` and the new dim path in `nodeColors` flow into `useEffect`s that call `invalidate()`, so prop-change-driven renders pump a frame.
…abel offset Three regressions from the engine port that the user flagged against the d3 reference screenshot: 1. **Document shape**: documents rendered as cubes / squares (boxGeometry). Switched to the same icosahedron geometry as concepts, distinguished only by scale and color — the d3 original drew them as larger circles too. Legend swatch follows (rounded-sm → rounded-full). 2. **Label color**: labels painted in the node's own color, so text on top of an amber node visually merged into the disc and the indigo extended-concept dots became unreadable — making them appear "missing" at a glance. Engine extension: NodeLabels accepts an optional `labelColors?: string[]` override (parallel to nodes), plumbed through Scene. Document Explorer paints all labels in `#e5e7eb` so they read against any node colour. Focus dim is baked into label colors the same way it is for node colors. 3. **Label position**: labels sat above nodes, hidden behind the larger document glyphs. Engine extension: NodeLabels accepts a signed `labelOffsetY` (plumbed via Scene as `nodeLabelOffsetY`); Document Explorer passes -2.2 to place labels below. Force Graph inherits the previous default (+1.4, above). Both engine props are optional; Force Graph behaviour is unchanged. The "missing extended-concept nodes" report should now resolve — they were there but their indigo labels visually merged into their indigo discs, making them look like a single coloured smear at concept-dot scale. Whitish labels + below-offset makes both the dots and their text readable.
Hoist the per-node scale resolution into `<Scene>` so node meshes and node labels consume the same `Float32Array`. The plugin override (`nodeScales`) still wins when provided; otherwise Scene derives the default from degree using the previously-internal formula. Effect: scale-aware label offsets now apply to Force Graph too, not just Document Explorer. Labels for high-degree hub nodes float past the node surface instead of sometimes sitting on top of it. The default `labelOffsetY = 1.4` is now interpreted as padding past radius rather than absolute world-units, so smaller nodes stay close and bigger nodes still clear cleanly. `<Nodes>` keeps its fallback path for the undefined-scales case so it remains usable as a leaf component if someone bypasses Scene, but in practice Scene always passes the resolved array now.
The workspace held its loaded graph (`explorerData`, `sidebarDocs`, `focusedDocId`) in local React state, so navigating away and back unmounted the workspace and reset to "Load a saved exploration query" empty state. Move those three slots into a small `documentExplorerStore`. The store lives in memory for the session and is not persisted to localStorage — mirrors Force Graph's policy for `rawGraphData` (stale snapshots are worse than no snapshot: a re-ingested DB or a different deploy makes node ids unreachable and the user clicks into a dead view). If we want cross-session persistence later, the saved exploration query is the durable record and the pipeline can replay it on next load, same as Force Graph's autosave. Pipeline state (`isLoading`, `error`, `loadingMessage`), settings, and the document viewer modal stay local — they're either truly ephemeral or workspace-mount-scoped UI.
…ttle, context menu Four UX regressions from the engine port, all in one commit since they share the click-handler flow: 1. **Constant "Settling..." indicator**: `physicsActive` initialized to `true` and only flipped off via the Reheat timeout — so the spinner ran forever on a fresh mount. The engine sim has no "settled" event (GPU loop keeps running by design), so the spinner is really just a Reheat acknowledgement. Default off; Reheat briefly turns it on. 2. **Info card invisible on left-click**: the port used the 2D corner `NodeInfoBox` instead of the engine's in-scene `NodeInfoOverlay` that Force Graph uses. Switched to the same `activeNodeInfos` / `onDismissNodeInfo` pattern — left-click on a concept pins an in-scene info card next to the node; clicking again dismisses it. 3. **Click on document didn't open the viewer**: the port required a double-click to open the viewer. Single-click now opens the viewer AND toggles focus (clicking the focused document again clears focus). The dblclick branch and timestamp bookkeeping are gone. 4. **Right-click context menu missing**: the wrapper div was `preventDefault`-ing all right-clicks, suppressing both the browser's native menu and anything else. Dropped the handler — the d3 original didn't override right-click either, so right-click falls through to the browser menu like before. A custom Document-Explorer context menu (e.g. "View document" / "Focus") is left for a follow-up if needed. Selected-concept state and the dblclick timestamp ref are gone — the info-overlay set is now the source of truth for "what concepts the user has pinned open".
…ck menu Match Force Graph's two-tier dim model and click semantics so muscle memory carries between the two explorers. **Hover** (transient, 0.25 dim): hovering any node activates the engine's `activeIds` to the node plus its graph neighbours. Edges and labels fade for everything else, baked into node colours the same way Force Graph does. Local-topology inspection without clicks. **Focus** (persistent, 0.05 dim): toggled via right-click → context menu. Documents focus to "document + all its concepts" (the previous behaviour); concepts can't be focused (no useful concept-level dim model yet — flagged for follow-up if needed). Focus wins over hover when both are active. **Left-click** is now pure inspection: - Click a document → opens the viewer (no longer toggles focus too) - Click a concept → toggles its in-scene `NodeInfoOverlay` **Right-click**: - On a document → "View document" / "Focus on document" / "Unfocus" - On a concept → "Unfocus" (when something is focused) - On background (when something is focused) → "Unfocus" `nodeContextConsumedRef` guards the wrapper's `onContextMenu` from opening a second menu after the node-mesh handler has already done so (same pattern Force Graph uses). The previous click-also-focuses behaviour was a double-event: clicking a document opened the viewer AND dimmed the rest of the graph, which the user reported as confusing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Document Explorer now renders on the same r3f + GPU engine as Force Graph. It keeps its visual experience (amber document glyphs, query/extended concept dots, focus dimming, info legend, NodeInfoBox) and gains the 2D ↔ 3D projection toggle. The d3 + SVG implementation that preceded this port is gone.
ADR-702 phase 4 — was framed as "optional/opportunistic" — now landed.
Commits
`a1b49e3b` `feat(web): per-node geometry class in the unified engine` — adds three optional, backward-compatible props to ``/``: `nodeClasses` (per-node class key), `geometryByClass` (geometry per class → one InstancedMesh per class), `nodeScales` (per-node base scale override). Force Graph doesn't pass any of these, so it renders identically to before.
`ede92ecb` `feat(web): port Document Explorer to the unified engine` — replaces the 604-line d3+SVG implementation with a ~340-line wrapper around ``. Documents render as boxGeometry, concepts as icosahedronGeometry. `DocumentExplorerWorkspace` keeps its direct-mount pattern (no changes to the workspace's prop shape).
`e93ad5ea` `fix(web): preserve document-concept clustering and bake focus dim into colors` — advisor-driven fixes after the first cut: adds `edgeVisible` to the engine so document→concept clustering hints can stay in the physics sim while rendering collapsed; bakes the focus-dim multiplier into the per-node color array so document and concept meshes recede in focus mode (engine `` doesn't read `activeIds`/`dimAlpha` directly).
Engine surface (the discriminator)
The advisor's discriminator for "is the abstraction right" was: can you delete the d3 implementation and replace it with a thin shim, without piling on engine hooks? Two engine-level additions did the job:
If a third explorer ever needs per-node opacity or per-class force tuning, those become the next hooks. For now, the engine surface stays bounded.
Manual verification (recommended before merge)
The vitest suite and `tsc --noEmit` are clean, but the runtime behaviour can't be unit-tested:
`kg-web-dev` HMR usually picks this up cleanly; if anything looks stale after pulling, `docker restart kg-web-dev`.
Behavior changes vs. d3 implementation
Deferred (carried through props for API stability)
References