diff --git a/.forgeplan/evidence/EVID-027-force-3d-smoke-test-threlte-canvas-simulation-theme-bundle.md b/.forgeplan/evidence/EVID-027-force-3d-smoke-test-threlte-canvas-simulation-theme-bundle.md deleted file mode 100644 index 7bb1da3..0000000 --- a/.forgeplan/evidence/EVID-027-force-3d-smoke-test-threlte-canvas-simulation-theme-bundle.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -depth: standard -id: EVID-027 -kind: evidence -last_modified_at: 2026-05-07T21:17:33.662131+00:00 -last_modified_by: claude-code/2.1.132 -links: -- target: PRD-022 - relation: informs -- target: RFC-019 - relation: informs -status: active -title: Force 3D smoke test — Threlte canvas, simulation, theme + bundle ---- - -# EVID-027: Force 3D smoke test — Threlte canvas, simulation, theme + bundle - -| Field | Value | -|-------|-------| -| Status | Draft | -| Created | 2026-05-07 | -| Valid Until | 2026-08-07 | -| Target | PRD-022 / RFC-019 | - -## Structured Fields - -verdict: supports -congruence_level: 3 -evidence_type: test - -## Summary - -End-to-end browser smoke test of the new `force3d` view mode (PRD-022 / -RFC-019) against the parent Forgeplan workspace (123 artifacts, 104 edges, -mixed kinds + statuses). Verifies: dropdown placement + experimental badge, -lazy-load isolation, 3D scene rendering, orbit camera, theme reactivity, -type-check, and prod build success. - -## Method - -1. `npm run check` — full svelte-check. -2. `npm run build` — Vite production build; inspect chunk graph. -3. `FORGEPLAN_CWD=$repo_root FORGEPLAN_BIN=/opt/homebrew/bin/forgeplan - npm run dev` — boot dev server on `127.0.0.1:5174`. -4. Browser drive (Chromium + claude-in-chrome MCP): - - open `http://127.0.0.1:5174` - - click pane view-mode dropdown → confirm "Force 3D" last with EXPERIMENTAL pill - - select Force 3D → wait for lazy chunk - - drag-orbit the canvas → confirm camera rotation - - toggle Light ↔ Dark theme → confirm canvas backdrop + node colors update - -## Observations - -| Probe | Expected | Observed | Verdict | -|-------|----------|----------|---------| -| `npm run check` | 0 errors | `2105 FILES 0 ERRORS 0 WARNINGS` | passes | -| `npm run build` | success | `built in 6.44s`; `dependency-graph-3d.js` server chunk = 17.95 kB; client chunk `BUVFnBlC.js` (Three+Threlte) = 793 kB | passes | -| Lazy import path in client | `import('../chunks/BUVFnBlC.js')` only on `view === 'force3d'` | grep over `nodes/2.CrgDLbMO.js` shows the chunk is referenced **only** behind the `view === 'force3d'` guard, never as a static import on the homepage entry | passes (NFR-003 / FR-009 / SC-4) | -| Dropdown order | Force 3D last | accessibility tree: option index 8 of 8, label `"Force 3D"`, badge `experimental`, accessible name `"Force 3D, experimental"` | passes (FR-006 / SC-5) | -| 3D rendering | spheres + edges visible | 123 spheres, 104 line segments, 3D parallax under orbit | passes (FR-002 / FR-003) | -| Orbit camera | drag rotates | drag (710,400) → (900,250) shifted layout consistent with rotation | passes (FR-004) | -| Theme reactivity | bg + node colors flip on `data-theme` change | confirmed: dark = near-black gradient + light nodes; light = cream gradient + dark nodes | passes (FR-007) | -| Mode parity (visible counts) | filteredNodes / filteredEdges = parent counts | 123 / 104 with empty filter set, identical to 2D Force | passes (AC-2) | - -## Limitations - -- Performance budgets (NFR-001 first-frame, NFR-002 fps) not yet collected - via DevTools Performance trace — visual smoke only. The UI is responsive - during orbit; capturing p50 fps across 5 runs is deferred to a follow-up - EvidencePack. -- Hover highlight (FR-005) is wired in code but `pointerenter` on - `` requires `@threlte/extras` `interactivity()` plugin — left as - TODO for the in-Force-mode 2D ↔ 3D switch follow-up (`force-mode-2d3d-switch` - TODO marker). -- WebGL fallback message exists but was not exercised (no headless WebGL- - disabled probe was run). - diff --git a/.forgeplan/prds/PRD-022-force-3d-experimental-3d-dependency-graph-view-mode-threlte.md b/.forgeplan/prds/PRD-022-force-3d-experimental-3d-dependency-graph-view-mode-threlte.md deleted file mode 100644 index f377a2d..0000000 --- a/.forgeplan/prds/PRD-022-force-3d-experimental-3d-dependency-graph-view-mode-threlte.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -depth: standard -id: PRD-022 -kind: prd -last_modified_at: 2026-05-07T20:54:26.426014+00:00 -last_modified_by: claude-code/2.1.132 -status: active -title: Force 3D — experimental 3D dependency-graph view mode (Threlte) ---- - ---- -id: PRD-022 -title: "Force 3D — experimental 3D dependency-graph view mode (Threlte)" -status: Draft -author: claude-code -created: 2026-05-07 -updated: 2026-05-07 -priority: P2 -depth: standard -domain: general -projectType: web_app -stepsCompleted: [] ---- - -# PRD-022: Force 3D — experimental 3D dependency-graph view mode (Threlte) - -## Progress - -``` -Phase 0 ████████████████████░░░░ 4/5 ( 80%) -───────────────────────────────────────────────── -TOTAL 4/5 ( 80%) -``` - ---- - -## Executive Summary - -### Vision - -A new opt-in **Force 3D** view mode that renders the same artifact dependency -graph in true 3D space, giving the explorer a navigable "constellation" of -PRDs / RFCs / ADRs / Specs / Evidence so that cluster topology is legible at -a glance even at 100+ artifacts. - -### Problem - -The current 2D `Force` view collapses dense workspaces into hairballs when -the artifact count crosses ~80 — clusters overlap, edges criss-cross, and -positional meaning is lost (PRD-018 / EVID-022 already shipped damping -mitigations, but the underlying 2D limitation remains). The explorer cannot -visually separate orthogonal sub-graphs without manually filtering by kind -or status. - -**Impact**: at the reference workspace (the parent Forgeplan repo, ~69 -artifacts) clusters touch each other; the screenshot bundled with this PRD -shows a representative dense graph where the central hairball obscures -detail. Adding more artifacts will only widen the gap between what the -viewer needs to see and what 2D physics can show. - -### Target Users - -| Persona | Описание | Ключевая боль | -|---------|----------|---------------| -| Forgeplan power user (daily) | Reviewer of large workspaces (50+ artifacts), uses Force view to find blind spots / orphans / clusters. | 2D hairballs hide topology; manual filtering takes time. | -| Curious onlooker (occasional) | Investor / new contributor exploring a project's artifact graph for orientation. | Wants something striking + understandable; flat 2D feels "engineering-only". | - -### Differentiators - -- 3D force-directed layout (3 axes × repulsion + link forces) — separable - clusters become spatially distinct. -- Forgeplan-native styling: token-driven colors per artifact `kind`, status - ring, `R_eff`-driven sphere size — same visual grammar as the 2D Force - view, not a generic gallery demo. -- Experimental opt-in (last in the dropdown, badged `experimental`) — no - regression risk for the default Force flow. - ---- - -## Success Criteria - -| ID | Criterion | Metric | Current | Target | Timeframe | How to Measure | -|----|-----------|--------|---------|--------|-----------|----------------| -| SC-1 | Force 3D renders same artifact set as Force | nodes(force_3d) === nodes(force) for the same `kindFilter` / `statusFilter` | n/a | 100 % match | Ship | Manual smoke against parent Forgeplan workspace (≥60 artifacts). | -| SC-2 | Initial frame budget on a 200-node graph | Time from mount to first stable frame | n/a | ≤ 1500 ms on M-class laptop, Chromium release | Ship | Browser DevTools Performance trace, p50 across 5 runs. | -| SC-3 | Steady-state frame rate while orbiting | rAF over 5 s of mouse-drag rotation | n/a | ≥ 45 fps on a 200-node graph | Ship | DevTools Performance "Frames" track. | -| SC-4 | Bundle size impact on default `dist/` shape | Difference in `dist/client/_app/immutable/` total gzip vs main | n/a | 0 bytes (lazy-loaded, code-split chunk) | Ship | `du -sb` on `dist/client/_app/immutable/` before/after on default mode. | -| SC-5 | Experimental badge visible | `experimental` badge rendered next to "Force 3D" in the view-mode dropdown | n/a | Visible in dropdown + accessible name | Ship | Manual + visual inspection against `/playground` toggle catalogue. | - ---- - -## Product Scope - -### MVP (In-Scope) - -- New `force3d` entry registered in `GRAPH_VIEWS` (last position) and `GraphView` union. -- New widget `widgets/dependency-graph-3d/` exposing `Force3DView.svelte` - (Threlte canvas + `d3-force-3d` simulation + node spheres + edge segments). -- `experimental` badge surfaced in the view-mode dropdown next to "Force 3D". -- Lazy-loaded chunk: Threlte / Three.js never enter the main bundle of - non-3D users. -- `R_eff` → sphere radius mapping consistent with 2D `r_eff` font emphasis. -- `kind` → token color (matches 2D `kindBorder`/`kindLabelColor`). -- `status` → emissive ring / outline per `statusRing`. -- Pointer hover → highlight neighbours + dim others (parity with 2D Force - semantics, no animation regressions). -- Camera: orbit + zoom (Three.js OrbitControls or equivalent Threlte abstraction). -- Reduced-motion preference honoured: simulation alpha decays faster. -- TODO marker in `ForceView.svelte` recording the future plan: a 2D ↔ 3D - switch *inside* the Force mode itself (so users don't have to drop out - to a separate dropdown entry). - -### Out of Scope - -- The in-Force-mode 2D ↔ 3D switch (recorded as TODO; separate PRD later). -- Mobile / touch optimisation (desktop-first; the badge says experimental). -- VR / WebXR mode. -- Custom shaders, post-processing, bloom / FXAA. -- Persistence of camera state across sessions (we reset on view switch in - parity with 2D Force). -- Per-artifact 3D label rendering (sprite labels are deferred — hover - tooltip only for MVP). - -### Growth Vision - -- Bring the visual upgrade into the **default** Force mode via a switch - (the recorded TODO). -- Edge bundling in 3D for very dense graphs. -- Time-axis: extrude `created_at` along Z so topology + chronology are - visible together. -- Persist camera + filter state in URL so a 3D constellation can be linked. - ---- - -## User Journeys - -### Journey 1: Daily reviewer wants to see cluster topology at a glance - -**Цель пользователя**: Spot orthogonal sub-graphs without filtering. - -| Шаг | Действие пользователя | Ответ системы | Заметки | -|-----|----------------------|---------------|---------| -| 1 | Open the workspace | HomePage shows Force 2D by default | No regression | -| 2 | Open the pane view-mode dropdown | Sees seven existing modes + "Force 3D" last with experimental badge | Discoverable | -| 3 | Selects "Force 3D" | Threlte canvas mounts, 3D simulation runs to settle | First-frame ≤ 1500 ms (SC-2) | -| 4 | Drags to orbit | Camera rotates, fps stable (SC-3) | Reduced motion respected | -| 5 | Hovers a node | Neighbours highlighted, others dim, tooltip shows id + title | Same hover semantics as 2D Force | - -**Результат**: cluster boundaries visible in 3D; user can identify isolated -sub-graphs without filter manipulation. - -### Journey 2: Onlooker explores forgeplan-web for the first time - -**Цель пользователя**: Get a striking, legible orientation view. - -| Шаг | Действие пользователя | Ответ системы | Заметки | -|-----|----------------------|---------------|---------| -| 1 | Lands on HomePage | Sees Force 2D (default) | Familiar | -| 2 | Opens the view dropdown | Sees the experimental Force 3D entry, last | Curiosity hook | -| 3 | Selects Force 3D | 3D scene builds, badge shows "experimental" — caveat is honest | Onlooker is informed | - -**Результат**: the user walks away with a clear "this project has structure" -takeaway, with the right caveat. - ---- - -## Functional Requirements - -| ID | Category | Priority | Requirement | Journey | -|----|----------|----------|-------------|---------| -| FR-001 | Core | Must | Reviewer can select "Force 3D" from the view-mode dropdown of any pane. | Journey 1, 2 | -| FR-002 | Core | Must | Reviewer can see all artifacts of the current `kindFilter` / `statusFilter` set as nodes in the 3D scene. | Journey 1 | -| FR-003 | Core | Must | Reviewer can see all dependency edges (informs / based_on / supersedes / refines / contradicts) as 3D segments. | Journey 1 | -| FR-004 | Core | Must | Reviewer can orbit and zoom the camera with the pointer. | Journey 1 | -| FR-005 | Core | Must | Reviewer can hover a node to see its id + title and have neighbours highlighted. | Journey 1 | -| FR-006 | Core | Should | Reviewer can read the `experimental` badge next to the "Force 3D" label in the dropdown. | Journey 2 | -| FR-007 | UX | Should | Reviewer can rely on the same color-by-kind / status-ring grammar already used in 2D Force. | Journey 1 | -| FR-008 | UX | Should | Reviewer can see node radius scaled by `R_eff` (parity with 2D emphasis). | Journey 1 | -| FR-009 | Performance | Must | The Force 3D chunk is lazy-loaded — non-3D users do not pay for it. | Journey 2 | - ---- - -## Non-Functional Requirements - -| ID | Category | Requirement | Metric | Condition | Measurement | -|----|----------|-------------|--------|-----------|-------------| -| NFR-001 | Performance | First stable frame after switching to Force 3D | ≤ 1500 ms p50 | 200-node graph, M-class laptop, Chromium release | DevTools Performance | -| NFR-002 | Performance | Steady-state fps while orbiting | ≥ 45 fps p50 | 200-node graph, drag-rotate 5 s | DevTools Frames track | -| NFR-003 | Bundle | Default `dist/` size | unchanged (Δ = 0 bytes for non-3D users) | Force 3D chunk loaded only on selection | `du -sb dist/client/_app/immutable/` baseline vs PR | -| NFR-004 | Robustness | Switching modes in either direction | 0 console errors | 50 cycles between Force 2D ↔ Force 3D | Manual + console assert | -| NFR-005 | A11y | Dropdown affordance | Keyboard reachable, label "Force 3D, experimental" | Tab + Enter | bits-ui Select baseline + axe DevTools | -| NFR-006 | Reduced motion | Simulation cooling | alpha decay 2× faster when `prefers-reduced-motion: reduce` | OS preference set | Manual | -| NFR-007 | Read-only proxy | No new `/api/*` endpoints | `grep -RIn "forgeplan" template/src/routes/api/` unchanged | Diff vs `develop` | Diff inspection (rule 22) | - ---- - -## Acceptance Criteria - -### AC-1: Lazy-loaded Force 3D chunk - -```gherkin -Given a fresh production build of forgeplan-web -When the user does NOT select Force 3D mode -Then the bundle byte-count of dist/client/_app/immutable/ matches the - pre-PR baseline (Force 3D code lives in its own chunk) -``` - -### AC-2: Mode parity - -```gherkin -Given a workspace of N artifacts -When the user is in Force mode then switches to Force 3D -Then the number of rendered nodes equals N (after the same filters) -And the number of rendered edges equals the number of rendered edges - in Force mode for the same filters -``` - -### AC-3: Experimental badge - -```gherkin -Given the view-mode dropdown is open -When the user inspects the entries -Then "Force 3D" is the last entry -And it carries an "experimental" badge visible to sighted users -And its accessible name reads "Force 3D, experimental" -``` - ---- - -## Dependencies - -| Dependency | Type | Status | Owner | -|-----------|------|--------|-------| -| `threlte` (Threlte v8 / Svelte 5) | Runtime | Available on npm | upstream | -| `three` | Runtime | Available on npm | upstream | -| `d3-force-3d` | Runtime | Available on npm | upstream | -| Existing graph data store | Internal | Active | this repo | - ---- - -## Risks & Mitigations - -| ID | Risk | Probability | Impact | Mitigation | Owner | -|----|------|-------------|--------|------------|-------| -| R-1 | Three.js bundle adds ~150KB gzip to the main bundle | Medium | Medium | Lazy-load via dynamic `import()` (FR-009 / NFR-003). Verify via `du -sb` baseline diff. | this repo | -| R-2 | WebGL not available in user environment | Low | Medium | Fallback message "Force 3D requires WebGL" rendered in the canvas slot; user can switch back to Force. | this repo | -| R-3 | Threlte v8 + Svelte 5 runes integration has rough edges | Medium | Medium | Stick to `` declarative components and a small `useTask` for sim ticks; avoid the experimental APIs. | this repo | -| R-4 | Hairballs are still hairballs in 3D for very dense central clusters | Low | Low | Reuse `forceClusterRepel` ported to 3 axes; document in the RFC that this is mitigation, not elimination. | this repo | - ---- - -## Affected Files - -- `template/src/shared/config/ui-prefs.ts` — add `force3d` to `GRAPH_VIEWS` + `GraphView` union, extend `GraphViewMeta` with optional `badge`. -- `template/src/shared/ui/select/Select.svelte` — render `item.badge` next to label. -- `template/src/widgets/dependency-graph-3d/**` — new widget (Threlte canvas + sim + theme). -- `template/src/widgets/dependency-graph/ui/DependencyGraph.svelte` — wire `force3d` branch with dynamic import. -- `template/src/widgets/dependency-graph/ui/ForceView.svelte` — `// TODO(force-mode-2d3d-switch)` marker for the future in-mode switch. -- `template/package.json` — add `threlte`, `three`, `d3-force-3d` to `dependencies` + their `@types/*` to devDeps. - -## Related Artifacts - -| Artifact | Relation | Status | -|----------|----------|--------| -| RFC-019 | Architecture proposal | Draft | -| EVID-XX | Browser smoke test | TBD | - ---- - -> **Next step**: forgeplan validate PRD-022; align with RFC-019; create EvidencePack after browser smoke. - - - diff --git a/.forgeplan/rfcs/RFC-019-force-3d-rendering-threlte-canvas-d3-force-3d-bridge.md b/.forgeplan/rfcs/RFC-019-force-3d-rendering-threlte-canvas-d3-force-3d-bridge.md deleted file mode 100644 index 6ba6104..0000000 --- a/.forgeplan/rfcs/RFC-019-force-3d-rendering-threlte-canvas-d3-force-3d-bridge.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -depth: standard -id: RFC-019 -kind: rfc -last_modified_at: 2026-05-07T20:54:28.518672+00:00 -last_modified_by: claude-code/2.1.132 -links: -- target: PRD-022 - relation: based_on -status: active -title: Force 3D rendering — Threlte canvas + d3-force-3d bridge ---- - ---- -id: RFC-019 -title: "Force 3D rendering — Threlte canvas + d3-force-3d bridge" -status: Draft -author: claude-code -created: 2026-05-07 -updated: 2026-05-07 -prd: PRD-022 -depth: standard ---- - -# RFC-019: Force 3D rendering — Threlte canvas + d3-force-3d bridge - -## Progress - -``` -Phase 1 ████████████████████░░░░ 4/5 ( 80%) -Phase 2 ░░░░░░░░░░░░░░░░░░░░░░░░ 0/3 ( 0%) -───────────────────────────────────────────────── -TOTAL 4/8 ( 50%) -``` - ---- - -## Summary - -Add a new `force3d` view mode that renders the dependency graph in a Threlte -canvas, simulating positions with `d3-force-3d` (the 3-axis sibling of -`d3-force` we already use). The chunk is lazy-loaded; non-3D users pay -nothing. - -## Motivation - -PRD-022 is the user-facing motivation. From an engineering standpoint: - -- The 2D `d3-force` simulation we already run is naturally extensible to a - third axis — `d3-force-3d` is API-compatible enough that the existing - link / charge / collide setup ports near-1:1. -- Threlte v8 provides Svelte 5-native (runes) bindings on top of Three.js, - so we get declarative scenes (``, ``) and a `useTask` for - per-frame work — no manual rAF loop, no manual cleanup. -- The view-mode plumbing is already a discriminated union (`GraphView`); a - new entry is a one-line append + a new branch in `DependencyGraph.svelte`. -- We can lazy-load the chunk with `await import(...)` so Three.js never - enters the main bundle (NFR-003). - -If we do nothing: dense workspaces remain hairballs in 2D; the only escape -is filtering, which is per-user effort. - -## Goals - -- A new `force3d` view mode, last in the dropdown, with an `experimental` badge. -- Same data shape as 2D Force (artifacts + edges + scores) — no new endpoints. -- Same visual grammar (kind colors, status rings, R_eff sizing). -- Lazy-loaded — zero bundle cost for users who never open Force 3D. -- Camera orbit + zoom out of the box. -- Hover highlight parity with 2D Force. -- Reduced-motion preference honoured. - -## Non-Goals - -- Persisting camera state across sessions / URLs (deferred). -- Custom shaders / bloom / FXAA / post-processing. -- Touch / mobile optimisation. -- VR / WebXR. -- An in-Force-mode 2D ↔ 3D switch (recorded as TODO; separate PRD). -- Per-node sprite labels (hover-only for MVP). - -## Options Considered - -### Option A: Threlte v8 + d3-force-3d (chosen) - -**Description**: Render with Threlte's declarative `` components, -simulate with `d3-force-3d` (drop-in 3D version of the 2D forces we already -use), bridge sim ticks → reactive `$state` arrays via Threlte's `useTask`. - -**Pros**: -- Svelte 5 runes-native; no foreign reactivity model. -- Declarative scene graph reads like the rest of our codebase. -- `d3-force-3d` reuses our knowledge of `d3-force` exactly. -- Smallest authoring surface — no React, no Vue, no DOM-imperative Three.js. - -**Cons**: -- Threlte v8 is recent — fewer Stack Overflow answers than r3f. -- Adds Three.js (≈150KB gzip) — must lazy-load. - -### Option B: Vanilla Three.js + d3-force-3d - -**Description**: Hand-rolled Three.js scene inside a Svelte component; -manual rAF loop; manual disposal. - -**Pros**: -- Smallest dependency footprint (no Threlte). -- Total control over the render loop. - -**Cons**: -- We re-implement Threlte's lifecycle / cleanup / reactivity bridge by hand. -- Cleanup-on-unmount is the #1 source of WebGL memory leaks; Threlte's - context handles it. -- Verbose; reads nothing like the rest of the codebase. - -### Option C: 3d-force-graph (the reference library) - -**Description**: Drop-in `3d-force-graph` (vasturiano) — a high-level -wrapper that builds the scene + simulation + camera for you. - -**Pros**: -- Fastest path to a working demo (literally `new ForceGraph3D(el)`). - -**Cons**: -- Vanilla-DOM API; we'd wrap it in Svelte for no gain. -- It pulls in **its own** copy of Three.js + d3-force-3d, no tree-shaking. -- Visual customisation is via callbacks (`nodeThreeObject(node)`) — fine, - but harder to keep in sync with our tokens and our `bits-ui` patterns. -- Doesn't fit Forgeplan's design language without heavy override; the user - brief says explicitly "more beautiful + adapted to forgeplan specifics", - not "ship the demo". - -### Option D: react-three-fiber - -**Description**: Use r3f via the Svelte/React boundary. - -**Pros**: Largest ecosystem. -**Cons**: Foreign reactivity model; adds React; defeats the rationale of a -Svelte-first repo. Rejected outright. - -## Trade-off Analysis - -| Критерий | A (Threlte) | B (Vanilla Three) | C (3d-force-graph) | D (r3f) | -|----------|-------------|-------------------|--------------------|---------| -| Complexity | Low | Medium-High | Lowest | High | -| Cost (bundle, gzip) | ~155KB (Three+Threlte) | ~145KB (Three only) | ~165KB (Three + lib) | +React | -| Scalability | High (declarative scene scales) | Medium | Limited (callbacks) | High | -| Migration risk | Low (Svelte 5 native) | Medium (manual cleanup) | Medium (wrapping library) | High (React) | -| Developer experience | Best (matches repo style) | Worst (imperative) | OK | Foreign | -| Operational burden | Same as 2D | Higher (manual disposal) | Same | High | -| Visual customisation | Full (declarative ``) | Full | Indirect (cb-driven) | Full | - -## Proposed Direction - -**Option A** — Threlte v8 + `d3-force-3d`. Lazy-loaded. Visual styling via -existing `kindBorder` / `statusRing` / `r_eff` mappings (so 2D ↔ 3D parity -is structural). Camera via Threlte's `` (or `@threlte/extras`). - -## Risks & Open Questions - -- **R-1 Bundle size**: Threlte + Three.js add ~155KB gzip. Mitigation: - dynamic `import()` in `DependencyGraph.svelte` so the chunk is only - fetched when Force 3D is selected. Verification: `du -sb - dist/client/_app/immutable/` against baseline (NFR-003). -- **R-2 SSR**: Three.js touches DOM globals. Mitigation: SvelteKit's - client-only branch; the `Force3DView` is mounted under `{#if browser}`. -- **R-3 Threlte v8 churn**: Threlte is on a fast cadence. Mitigation: pin - exact version in `dependencies`; quarterly bump as a chore. -- **R-4 Hover edge highlight**: `` doesn't support per-segment - hover-state cheaply. Mitigation: rebuild the relevant `BufferGeometry` - on hover (cheap at 200 nodes). -- **Open**: do we need a minimap in 3D? **A**: not for MVP — the 2D Minimap - doesn't generalise; defer. -- **Open**: do we render labels? **A**: not for MVP — sprite labels at 200 - nodes are noise; tooltip-on-hover only. - -## Implementation Phases - -### Phase 1: Wiring + minimum viable 3D -- [x] **1.1** Add `threlte`, `three`, `d3-force-3d`, types to `template/package.json`. -- [x] **1.2** Extend `GraphViewMeta` with optional `badge`; register `force3d` last in `GRAPH_VIEWS` + `GraphView`. -- [x] **1.3** New widget `widgets/dependency-graph-3d/` (Threlte canvas, sim, spheres, edges, OrbitControls). -- [x] **1.4** Wire dynamic-import branch in `DependencyGraph.svelte`. -- [ ] **1.5** Render badge in `Select.svelte` (next to label). - -### Phase 2: Polish + parity -- [ ] **2.1** Hover highlight parity (neighbours bright, others dim). -- [ ] **2.2** Reduced-motion: alpha decay 2× faster. -- [ ] **2.3** TODO marker in `ForceView.svelte` for the future in-mode switch. - -## Affected Files - -- `template/package.json` — add `threlte`, `three`, `d3-force-3d`, `@types/three`, `@types/d3-force-3d`. -- `template/src/shared/config/ui-prefs.ts` — `force3d` entry + `badge?: string` field. -- `template/src/shared/ui/select/Select.svelte` — render `item.badge` next to label, in trigger and dropdown. -- `template/src/widgets/dependency-graph-3d/{ui/Force3DView.svelte,ui/index.ts,index.ts,model/types.ts,lib/sim-3d.ts,lib/theme-3d.ts}`. -- `template/src/widgets/dependency-graph/ui/DependencyGraph.svelte` — `force3d` branch with `await import(...)`. -- `template/src/widgets/dependency-graph/ui/ForceView.svelte` — `// TODO(force-mode-2d3d-switch)`. - -## Related Artifacts - -| Artifact | Type | Relation | -|----------|------|----------| -| PRD-022 | PRD | based_on | -| EVID-XX | EvidencePack | informs (after smoke) | - ---- - -> **Next step**: validate, build, smoke, evidence, activate. - - - diff --git a/scripts/build.mjs b/scripts/build.mjs index 6a27bfd..17772fc 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -22,14 +22,7 @@ const DIST_EXPERIMENTAL = join(ROOT, "dist-experimental"); // PRD-014 / RFC-013: cap for the experimental bundled dist. If esbuild // starts pulling more (svelte upgrade, accidental client-side imports), // fail loudly instead of silently bloating the tarball. -// PRD-022 / RFC-019: bumped 3M → 6M for the Force 3D view mode — Threlte -// + Three.js add ~2.4 MB unzipped to the inlined ESM bundle. The lazy -// chunk in legacy `dist/` keeps non-3D users at 0 bytes; the experimental -// single-file shape can't code-split, so we accept the larger floor here. -// TODO(force-3d-experimental-shape): revisit when --experimental graduates -// (RFC-013 graduation TODO) — at that point the legacy `dist/` is dropped -// and we may need to reintroduce code-splitting for the experimental shape. -const DIST_EXPERIMENTAL_MAX_BYTES = 6 * 1024 * 1024; +const DIST_EXPERIMENTAL_MAX_BYTES = 3 * 1024 * 1024; const args = new Set(process.argv.slice(2)); const CLEAN_ONLY = args.has("--clean"); diff --git a/template/package-lock.json b/template/package-lock.json index 0d568e0..f3e7098 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -10,11 +10,8 @@ "dependencies": { "@lucide/svelte": "^1.14.0", "@sveltejs/kit": "^2.59.0", - "@threlte/core": "^8.5.11", - "@threlte/extras": "^9.15.1", "bits-ui": "^2.18.1", "d3-force": "^3.0.0", - "d3-force-3d": "^3.0.6", "d3-hierarchy": "^3.1.2", "d3-sankey": "^0.12.3", "d3-selection": "^3.0.0", @@ -23,8 +20,7 @@ "dompurify": "^3.4.2", "marked": "^18.0.3", "svelte": "^5.55.5", - "svelte-sonner": "^1.1.1", - "three": "^0.184.0" + "svelte-sonner": "^1.1.1" }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", @@ -38,7 +34,6 @@ "@types/d3-zoom": "^3.0.8", "@types/dompurify": "^3.0.5", "@types/node": "^20.17.0", - "@types/three": "^0.184.1", "happy-dom": "^20.9.0", "svelte-check": "^4.4.7", "typescript": "^6.0.3", @@ -49,12 +44,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "license": "Apache-2.0" - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1003,55 +992,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@threejs-kit/instanced-sprite-mesh": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@threejs-kit/instanced-sprite-mesh/-/instanced-sprite-mesh-2.5.1.tgz", - "integrity": "sha512-pmt1ALRhbHhCJQTj2FuthH6PeLIeaM4hOuS2JO3kWSwlnvx/9xuUkjFR3JOi/myMqsH7pSsLIROSaBxDfttjeA==", - "dependencies": { - "diet-sprite": "^0.0.1", - "earcut": "^2.2.4", - "maath": "^0.10.7", - "three-instanced-uniforms-mesh": "^0.52.4", - "troika-three-utils": "^0.52.4" - }, - "peerDependencies": { - "three": ">=0.170.0" - } - }, - "node_modules/@threlte/core": { - "version": "8.5.11", - "resolved": "https://registry.npmjs.org/@threlte/core/-/core-8.5.11.tgz", - "integrity": "sha512-2IyYlrXAWG0c6UnBLnE6lBzaVfiPPF5nsGte3HIc6domRna4w9gG4WIYDnJ1FH8wcSnaq7afztbrpF01Y9m5vg==", - "license": "MIT", - "peerDependencies": { - "svelte": ">=5", - "three": ">=0.160" - } - }, - "node_modules/@threlte/extras": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/@threlte/extras/-/extras-9.15.1.tgz", - "integrity": "sha512-+AhS+caKH9WIawwSWWYOlbqZzxPNW+kQ3PT9y4NyzzscCcVUPoFpZwd0izy8KEAIKM34J1uMs36cZff6U6LEjQ==", - "license": "MIT", - "dependencies": { - "@threejs-kit/instanced-sprite-mesh": "^2.5.1", - "camera-controls": "^3.1.2", - "three-mesh-bvh": "^0.9.1", - "three-perf": "^1.0.11", - "three-viewport-gizmo": "^2.2.0", - "troika-three-text": "^0.52.4" - }, - "peerDependencies": { - "svelte": ">=5", - "three": ">=0.160" - } - }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1222,38 +1162,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT" - }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -1434,15 +1348,6 @@ "node": ">= 0.4" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/bits-ui": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", @@ -1467,19 +1372,6 @@ "svelte": "^5.33.0" } }, - "node_modules/camera-controls": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", - "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", - "license": "MIT", - "engines": { - "node": ">=22.0.0", - "npm": ">=10.5.1" - }, - "peerDependencies": { - "three": ">=0.126.1" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1551,12 +1443,6 @@ "internmap": "^1.0.0" } }, - "node_modules/d3-binarytree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", - "license": "MIT" - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -1611,22 +1497,6 @@ "node": ">=12" } }, - "node_modules/d3-force-3d": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", - "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", - "license": "MIT", - "dependencies": { - "d3-binarytree": "1", - "d3-dispatch": "1 - 3", - "d3-octree": "1", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-hierarchy": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", @@ -1648,12 +1518,6 @@ "node": ">=12" } }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", @@ -1795,12 +1659,6 @@ "integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==", "license": "MIT" }, - "node_modules/diet-sprite": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/diet-sprite/-/diet-sprite-0.0.1.tgz", - "integrity": "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A==", - "license": "ISC" - }, "node_modules/dompurify": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", @@ -1810,12 +1668,6 @@ "@types/trusted-types": "^2.0.7" } }, - "node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC" - }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1903,12 +1755,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2282,16 +2128,6 @@ "lz-string": "bin/bin.js" } }, - "node_modules/maath": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", - "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", - "license": "MIT", - "peerDependencies": { - "@types/three": ">=0.134.0", - "three": ">=0.134.0" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2313,12 +2149,6 @@ "node": ">= 20" } }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "license": "MIT" - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2440,15 +2270,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2772,55 +2593,6 @@ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, - "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", - "license": "MIT" - }, - "node_modules/three-instanced-uniforms-mesh": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/three-instanced-uniforms-mesh/-/three-instanced-uniforms-mesh-0.52.4.tgz", - "integrity": "sha512-YwDBy05hfKZQtU+Rp0KyDf9yH4GxfhxMbVt9OYruxdgLfPwmDG5oAbGoW0DrKtKZSM3BfFcCiejiOHCjFBTeng==", - "license": "MIT", - "dependencies": { - "troika-three-utils": "^0.52.4" - }, - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/three-mesh-bvh": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.9.tgz", - "integrity": "sha512-FJKitcjvbALmeQRK+Sc+nLGorCpkrZBrbgJZFzhdyWboak37DZikn46hvQkNqSbJPm227ahYmS6k3N/GXaAyXw==", - "license": "MIT", - "peerDependencies": { - "three": ">= 0.159.0" - } - }, - "node_modules/three-perf": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/three-perf/-/three-perf-1.0.11.tgz", - "integrity": "sha512-OgBpZjwL+csQKGKZjpkH/QHdbGFMxqngMbSEJeSnVNfXDYd6On7WHNv/GhUZH4YxIpNMwMahBWrNnsJvnbSJHQ==", - "license": "MIT", - "dependencies": { - "troika-three-text": "^0.52.0", - "tweakpane": "^3.1.10" - }, - "peerDependencies": { - "three": ">=0.170" - } - }, - "node_modules/three-viewport-gizmo": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/three-viewport-gizmo/-/three-viewport-gizmo-2.2.0.tgz", - "integrity": "sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.162.0 <1.0.0" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2873,51 +2645,12 @@ "node": ">=6" } }, - "node_modules/troika-three-text": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", - "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.52.4", - "troika-worker-utils": "^0.52.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-three-utils": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", - "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-worker-utils": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", - "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", - "license": "MIT" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tweakpane": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.10.tgz", - "integrity": "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/cocopon" - } - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -3125,12 +2858,6 @@ } } }, - "node_modules/webgl-sdf-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", - "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", - "license": "MIT" - }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", diff --git a/template/package.json b/template/package.json index 5a66c3c..e5eb629 100644 --- a/template/package.json +++ b/template/package.json @@ -26,7 +26,6 @@ "@types/d3-zoom": "^3.0.8", "@types/dompurify": "^3.0.5", "@types/node": "^20.17.0", - "@types/three": "^0.184.1", "happy-dom": "^20.9.0", "svelte-check": "^4.4.7", "typescript": "^6.0.3", @@ -36,11 +35,8 @@ "dependencies": { "@lucide/svelte": "^1.14.0", "@sveltejs/kit": "^2.59.0", - "@threlte/core": "^8.5.11", - "@threlte/extras": "^9.15.1", "bits-ui": "^2.18.1", "d3-force": "^3.0.0", - "d3-force-3d": "^3.0.6", "d3-hierarchy": "^3.1.2", "d3-sankey": "^0.12.3", "d3-selection": "^3.0.0", @@ -49,8 +45,7 @@ "dompurify": "^3.4.2", "marked": "^18.0.3", "svelte": "^5.55.5", - "svelte-sonner": "^1.1.1", - "three": "^0.184.0" + "svelte-sonner": "^1.1.1" }, "overrides": { "cookie": ">=0.7.0" diff --git a/template/src/shared/config/ui-prefs.ts b/template/src/shared/config/ui-prefs.ts index 4afae87..333760d 100644 --- a/template/src/shared/config/ui-prefs.ts +++ b/template/src/shared/config/ui-prefs.ts @@ -6,7 +6,6 @@ import Grid3x3 from "@lucide/svelte/icons/grid-3x3"; import Columns3 from "@lucide/svelte/icons/columns-3"; import Spline from "@lucide/svelte/icons/spline"; import Donut from "@lucide/svelte/icons/donut"; -import Box from "@lucide/svelte/icons/box"; type IconComponent = Component<{ size?: number | string; class?: string }>; @@ -15,7 +14,6 @@ export interface GraphViewMeta { label: string; hint: string; icon: IconComponent; - badge?: string; } export const GRAPH_VIEWS: GraphViewMeta[] = [ @@ -31,13 +29,6 @@ export const GRAPH_VIEWS: GraphViewMeta[] = [ hint: "Nested radial hierarchy partition", icon: Donut, }, - { - id: "force3d", - label: "Force 3D", - hint: "Physics-driven exploration in 3D space", - icon: Box, - badge: "experimental", - }, ]; export type GraphView = @@ -47,8 +38,7 @@ export type GraphView = | "matrix" | "lanes" | "sankey" - | "sunburst" - | "force3d"; + | "sunburst"; export const GRAPH_VIEW_IDS = new Set(GRAPH_VIEWS.map((v) => v.id)); diff --git a/template/src/shared/ui/select/Select.svelte b/template/src/shared/ui/select/Select.svelte index 0443bc7..75ae131 100644 --- a/template/src/shared/ui/select/Select.svelte +++ b/template/src/shared/ui/select/Select.svelte @@ -12,7 +12,6 @@ icon?: IconComponent; hint?: string; disabled?: boolean; - badge?: string; } interface Props { @@ -66,9 +65,6 @@ {/if} {active?.label ?? placeholder} - {#if active?.badge} - {active.badge} - {/if} {/if} @@ -94,12 +90,7 @@ {/if} - - {item.label} - {#if item.badge} - {item.badge} - {/if} - + {item.label} {#if item.hint} {item.hint} {/if} @@ -268,23 +259,4 @@ justify-content: center; color: var(--accent); } - - :global(.select-badge) { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 1px 5px; - margin-left: 6px; - border-radius: 999px; - border: 1px solid var(--accent); - color: var(--accent); - background: color-mix(in srgb, var(--accent) 8%, transparent); - font-family: var(--font-mono); - font-size: 9px; - line-height: 1; - letter-spacing: 0.04em; - text-transform: uppercase; - white-space: nowrap; - flex: 0 0 auto; - } diff --git a/template/src/widgets/dependency-graph-3d/index.ts b/template/src/widgets/dependency-graph-3d/index.ts deleted file mode 100644 index f22cb72..0000000 --- a/template/src/widgets/dependency-graph-3d/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Force3DView } from './ui/Force3DView.svelte'; -export type { Force3DProps } from './model/types'; diff --git a/template/src/widgets/dependency-graph-3d/lib/d3-force-3d.d.ts b/template/src/widgets/dependency-graph-3d/lib/d3-force-3d.d.ts deleted file mode 100644 index ccb9809..0000000 --- a/template/src/widgets/dependency-graph-3d/lib/d3-force-3d.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -// TODO(d3-force-3d-types): upstream d3-force-3d ships no .d.ts and no DT package -// exists. Mirroring the public API we use here; widen as we adopt more forces. -declare module 'd3-force-3d' { - export interface Simulation { - nodes(nodes: NodeDatum[]): this; - nodes(): NodeDatum[]; - alpha(value: number): this; - alpha(): number; - alphaDecay(value: number): this; - alphaTarget(value: number): this; - velocityDecay(value: number): this; - force(name: string, force?: F | null): this; - on(event: 'tick' | 'end', listener: () => void): this; - tick(iterations?: number): this; - stop(): this; - restart(): this; - numDimensions(n: 1 | 2 | 3): this; - } - export interface ForceLink { - (alpha: number): void; - links(links?: LinkDatum[]): this; - id(accessor: (d: NodeDatum) => string | number): this; - distance(value: number | ((link: LinkDatum) => number)): this; - strength(value: number | ((link: LinkDatum) => number)): this; - iterations(n: number): this; - } - export interface ForceManyBody { - (alpha: number): void; - strength(value: number): this; - distanceMin(value: number): this; - distanceMax(value: number): this; - theta(value: number): this; - } - export interface ForceCenter { - (alpha: number): void; - strength(value: number): this; - } - - export function forceSimulation( - nodes?: NodeDatum[], - numDimensions?: 1 | 2 | 3, - ): Simulation; - export function forceLink( - links?: LinkDatum[], - ): ForceLink; - export function forceManyBody(): ForceManyBody; - export function forceCenter( - x?: number, - y?: number, - z?: number, - ): ForceCenter; - export function forceCollide( - radius?: number | ((d: NodeDatum) => number), - ): { - (alpha: number): void; - radius(value: number | ((d: NodeDatum) => number)): unknown; - strength(value: number): unknown; - iterations(n: number): unknown; - }; -} diff --git a/template/src/widgets/dependency-graph-3d/lib/sim-3d.ts b/template/src/widgets/dependency-graph-3d/lib/sim-3d.ts deleted file mode 100644 index e5a1198..0000000 --- a/template/src/widgets/dependency-graph-3d/lib/sim-3d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - forceCenter, - forceLink, - forceManyBody, - forceSimulation, - type Simulation, -} from 'd3-force-3d'; -import type { Sim3DLink, Sim3DNode } from '../model/types'; - -export interface Sim3DConfig { - reducedMotion: boolean; -} - -export function buildSimulation( - nodes: Sim3DNode[], - links: Sim3DLink[], - config: Sim3DConfig, -): Simulation { - const sim = forceSimulation(nodes, 3) - .force( - 'link', - forceLink(links) - .id((d: Sim3DNode) => d.id) - .distance(22) - .strength(0.6), - ) - .force('charge', forceManyBody().strength(-65).distanceMax(220)) - .force('center', forceCenter(0, 0, 0).strength(0.08)) - .alpha(1) - .alphaDecay(config.reducedMotion ? 0.05 : 0.025) - .velocityDecay(0.4); - - return sim; -} diff --git a/template/src/widgets/dependency-graph-3d/lib/theme-3d.ts b/template/src/widgets/dependency-graph-3d/lib/theme-3d.ts deleted file mode 100644 index ac39fdb..0000000 --- a/template/src/widgets/dependency-graph-3d/lib/theme-3d.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Color } from 'three'; - -const ACCENT_KINDS = new Set(['epic', 'problem']); -const EVIDENCE_KINDS = new Set(['evidence', 'evid']); - -const FALLBACK = { - bg: '#0a0a0f', - fg: '#e6e6f0', - accent: '#ff7a18', - good: '#3ddc84', - neutral: '#9aa0a6', - fg3: '#5a5d66', - line: '#2a2a35', -}; - -function readVar(name: string, fallback: string): string { - if (typeof window === 'undefined') return fallback; - const v = getComputedStyle(document.documentElement) - .getPropertyValue(name) - .trim(); - return v.length > 0 ? v : fallback; -} - -export interface Theme3DPalette { - bg: Color; - accent: Color; - good: Color; - neutral: Color; - fg3: Color; - line: Color; -} - -export function readTheme3D(): Theme3DPalette { - return { - bg: new Color(readVar('--bg', FALLBACK.bg)), - accent: new Color(readVar('--accent', FALLBACK.accent)), - good: new Color(readVar('--accent-good', FALLBACK.good)), - neutral: new Color(readVar('--fg-1', FALLBACK.neutral)), - fg3: new Color(readVar('--fg-3', FALLBACK.fg3)), - line: new Color(readVar('--line-2', FALLBACK.line)), - }; -} - -export function nodeColor(palette: Theme3DPalette, kind: string): Color { - const k = kind.toLowerCase(); - if (EVIDENCE_KINDS.has(k)) return palette.good; - if (ACCENT_KINDS.has(k)) return palette.accent; - return palette.neutral; -} - -const STATUS_GLOW: Record = { - active: 'good', - draft: 'accent', - superseded: 'fg3', - deprecated: 'fg3', - stale: 'accent', -}; - -export function statusGlow(palette: Theme3DPalette, status: string): Color { - const tone = STATUS_GLOW[status.toLowerCase()] ?? 'fg3'; - return palette[tone]; -} - -export function nodeRadius(rEff: number): number { - const safe = Number.isFinite(rEff) ? rEff : 0; - const t = Math.max(0, Math.min(1, safe)); - return 1.8 + t * 2.0; -} diff --git a/template/src/widgets/dependency-graph-3d/model/types.ts b/template/src/widgets/dependency-graph-3d/model/types.ts deleted file mode 100644 index 651adb8..0000000 --- a/template/src/widgets/dependency-graph-3d/model/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ArtifactSummary } from '@/entities/artifact'; -import type { GraphEdge } from '@/entities/graph'; -import type { ScoreEntry } from '@/entities/score'; - -export interface Force3DProps { - nodes?: ArtifactSummary[]; - edges?: GraphEdge[]; - scores?: ScoreEntry[]; - selectedId?: string | null; - kindFilter?: Set; - statusFilter?: Set; - onSelect?: (detail: { id: string }) => void; - onViewState?: (state: { - nodes: Array<{ id: string; x: number; y: number; kind: string }>; - transform: { x: number; y: number; k: number }; - viewport: { w: number; h: number }; - }) => void; -} - -export interface Sim3DNode { - id: string; - kind: string; - status: string; - title: string; - r_eff: number; - x: number; - y: number; - z: number; - vx?: number; - vy?: number; - vz?: number; - index?: number; -} - -export interface Sim3DLink { - source: string | Sim3DNode; - target: string | Sim3DNode; - relation: string; -} diff --git a/template/src/widgets/dependency-graph-3d/ui/Force3DView.svelte b/template/src/widgets/dependency-graph-3d/ui/Force3DView.svelte deleted file mode 100644 index efc9c7f..0000000 --- a/template/src/widgets/dependency-graph-3d/ui/Force3DView.svelte +++ /dev/null @@ -1,155 +0,0 @@ - - -
- {#if !browser} -
Loading 3D scene…
- {:else if !webglAvailable} -
- Force 3D requires WebGL — falling back is not yet wired. -
- {:else} - - - - {/if} -
- drag - orbit - · - scroll - zoom - · - click - select -
-
- - diff --git a/template/src/widgets/dependency-graph-3d/ui/NodeMesh.svelte b/template/src/widgets/dependency-graph-3d/ui/NodeMesh.svelte deleted file mode 100644 index c344410..0000000 --- a/template/src/widgets/dependency-graph-3d/ui/NodeMesh.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - - void }) => { - ev.stopPropagation?.(); - onClick(); - }} -> - - - {#if selected} - - - - - {/if} - diff --git a/template/src/widgets/dependency-graph-3d/ui/Scene3D.svelte b/template/src/widgets/dependency-graph-3d/ui/Scene3D.svelte deleted file mode 100644 index d6a635b..0000000 --- a/template/src/widgets/dependency-graph-3d/ui/Scene3D.svelte +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - -{#each simNodes as node, i (node.id)} - { - nodeRefs[i] = mesh; - }} - onPointerEnter={() => handlePointerOver(node.id)} - onPointerLeave={handlePointerOut} - onClick={() => handleClick(node.id)} - /> -{/each} diff --git a/template/src/widgets/dependency-graph/ui/DependencyGraph.svelte b/template/src/widgets/dependency-graph/ui/DependencyGraph.svelte index 554179d..99a1c80 100644 --- a/template/src/widgets/dependency-graph/ui/DependencyGraph.svelte +++ b/template/src/widgets/dependency-graph/ui/DependencyGraph.svelte @@ -12,12 +12,6 @@ import SunburstView from './SunburstView.svelte'; import Minimap from './Minimap.svelte'; - const Force3DLazy = $derived.by(() => - view === 'force3d' - ? import('@/widgets/dependency-graph-3d').then((m) => m.Force3DView) - : null, - ); - let { view = 'force', nodes = [], @@ -158,26 +152,6 @@ onSelect={relay} {onViewState} /> - {:else if view === 'force3d'} - {#await Force3DLazy} -
Loading Force 3D…
- {:then Force3DView} - {#if Force3DView} - - {/if} - {:catch err} -
Failed to load 3D scene: {(err as Error).message}
- {/await} {:else} diff --git a/template/src/widgets/dependency-graph/ui/ForceView.svelte b/template/src/widgets/dependency-graph/ui/ForceView.svelte index 50dcfc2..a7297c6 100644 --- a/template/src/widgets/dependency-graph/ui/ForceView.svelte +++ b/template/src/widgets/dependency-graph/ui/ForceView.svelte @@ -36,11 +36,6 @@ import { forceClusterRepel } from '../lib/force-cluster-repel'; import { pickNextNode, type Direction } from '../lib/keyboard-nav'; - // TODO(force-mode-2d3d-switch): expose a Force-mode 2D ↔ 3D toggle (PRD-022 / - // RFC-019 growth vision) so users don't have to drop out to a separate - // "Force 3D" entry to see the 3D layout. Today the modes live as siblings - // in GRAPH_VIEWS; this becomes a per-pane toggle owned by ForceView. - interface Node extends SimulationNodeDatum { id: string; kind: string; diff --git a/template/src/widgets/mosaic/ui/PaneFrame.svelte b/template/src/widgets/mosaic/ui/PaneFrame.svelte index 49a3ca6..092d6f1 100644 --- a/template/src/widgets/mosaic/ui/PaneFrame.svelte +++ b/template/src/widgets/mosaic/ui/PaneFrame.svelte @@ -31,7 +31,6 @@ label: v.label, icon: v.icon, hint: v.hint, - badge: v.badge, })); function onDragStart(e: DragEvent) {