@stephen-netu/design-system 0.1.0
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/README.md +66 -0
- package/package.json +130 -0
- package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
- package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
- package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
- package/src/b0-app-shell/index.ts +14 -0
- package/src/base.css +152 -0
- package/src/d0-data-viz/BarChart.svelte +236 -0
- package/src/d0-data-viz/CheckpointBar.svelte +574 -0
- package/src/d0-data-viz/CheckpointPill.svelte +297 -0
- package/src/d0-data-viz/MetricCard.svelte +159 -0
- package/src/d0-data-viz/SparkLine.svelte +66 -0
- package/src/d0-data-viz/StorageMap.svelte +182 -0
- package/src/d0-data-viz/Timeline.svelte +188 -0
- package/src/d0-data-viz/checkpoint-types.ts +162 -0
- package/src/d0-data-viz/d0-tokens.css +32 -0
- package/src/d0-data-viz/index.ts +58 -0
- package/src/d0-data-viz/storage-map-types.ts +142 -0
- package/src/d0-data-viz/theme-integration.ts +134 -0
- package/src/d0-data-viz/types.ts +53 -0
- package/src/demos/BspSizingInfographic.svelte +386 -0
- package/src/demos/BspZoneDemo.svelte +255 -0
- package/src/demos/ControlRoomDemo.svelte +570 -0
- package/src/demos/NodepadBlocksShowcase.svelte +189 -0
- package/src/demos/index.ts +7 -0
- package/src/editor/CodeEditor.svelte +188 -0
- package/src/editor/FindReplaceDialog.svelte +516 -0
- package/src/editor/MarkdownEditor.svelte +660 -0
- package/src/editor/index.ts +2 -0
- package/src/entity-graph-view/WorldView.svelte +171 -0
- package/src/entity-graph-view/index.ts +6 -0
- package/src/entity-graph-view/types.ts +364 -0
- package/src/epistemic/index.ts +2 -0
- package/src/epistemic/types.ts +134 -0
- package/src/epistemic/utils.ts +32 -0
- package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
- package/src/f0-forms/field/FormField.svelte +91 -0
- package/src/f0-forms/index.ts +11 -0
- package/src/f0-forms/radio/RadioGroup.svelte +192 -0
- package/src/f0-forms/search/SearchBar.svelte +99 -0
- package/src/f0-forms/select/Select.svelte +198 -0
- package/src/f0-forms/textarea/TextArea.svelte +170 -0
- package/src/f0-forms/toggle/FormToggle.svelte +147 -0
- package/src/f0-forms/validation/validate.ts +47 -0
- package/src/fabric/ai/ChatPanel.svelte +648 -0
- package/src/fabric/ai/index.ts +11 -0
- package/src/fabric/data/index.ts +21 -0
- package/src/fabric/feedback/StatusBar.svelte +274 -0
- package/src/fabric/feedback/ToastContainer.svelte +122 -0
- package/src/fabric/feedback/index.ts +14 -0
- package/src/fabric/index.ts +37 -0
- package/src/fabric/input/CommandBar.svelte +79 -0
- package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
- package/src/fabric/input/index.ts +12 -0
- package/src/fabric/layout/GlassBox.svelte +193 -0
- package/src/fabric/layout/KanbanBoard.svelte +158 -0
- package/src/fabric/layout/KanbanCard.svelte +226 -0
- package/src/fabric/layout/KanbanColumn.svelte +278 -0
- package/src/fabric/layout/index.ts +17 -0
- package/src/fabric/navigation/ActivityRail.svelte +271 -0
- package/src/fabric/navigation/index.ts +11 -0
- package/src/g0-geo/SovereignMap.svelte +149 -0
- package/src/g0-geo/index.ts +2 -0
- package/src/index.ts +68 -0
- package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
- package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
- package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
- package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
- package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
- package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
- package/src/l0-layout/block-writer/index.ts +24 -0
- package/src/l0-layout/block-writer/markdown.ts +110 -0
- package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
- package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
- package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
- package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
- package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
- package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
- package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
- package/src/l0-layout/epistemic/TrustView.svelte +195 -0
- package/src/l0-layout/epistemic/index.ts +7 -0
- package/src/l0-layout/grove/AccordCard.svelte +156 -0
- package/src/l0-layout/grove/GroveCard.svelte +197 -0
- package/src/l0-layout/grove/MemberBadge.svelte +96 -0
- package/src/l0-layout/grove/MemberList.svelte +59 -0
- package/src/l0-layout/grove/grove-types.ts +29 -0
- package/src/l0-layout/grove/index.ts +14 -0
- package/src/l0-layout/index.ts +45 -0
- package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
- package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
- package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
- package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
- package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
- package/src/n0-node-graph/GraphEditor.svelte +342 -0
- package/src/n0-node-graph/GraphHistory.svelte +122 -0
- package/src/n0-node-graph/Node.svelte +269 -0
- package/src/n0-node-graph/NodeCanvas.svelte +187 -0
- package/src/n0-node-graph/NodeEdge.svelte +152 -0
- package/src/n0-node-graph/NodePalette.svelte +318 -0
- package/src/n0-node-graph/NodePort.svelte +128 -0
- package/src/n0-node-graph/index.ts +33 -0
- package/src/n0-node-graph/n0-tokens.css +29 -0
- package/src/n0-node-graph/node-port-types.ts +19 -0
- package/src/n0-node-graph/types.ts +96 -0
- package/src/n0-node-graph/validate.test.ts +722 -0
- package/src/n0-node-graph/validate.ts +371 -0
- package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
- package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
- package/src/n1-flow-canvas/index.ts +21 -0
- package/src/n1-flow-canvas/layout.ts +118 -0
- package/src/n1-flow-canvas/layout.worker.ts +128 -0
- package/src/n1-flow-canvas/types.ts +102 -0
- package/src/n1-flow-canvas/utils.ts +146 -0
- package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
- package/src/n2-manifold-disk/index.ts +6 -0
- package/src/n2-manifold-disk/poincare.ts +184 -0
- package/src/n3-force-graph/ForceGraph.svelte +180 -0
- package/src/n3-force-graph/GraphTooltip.svelte +99 -0
- package/src/n3-force-graph/animated-edge.ts +107 -0
- package/src/n3-force-graph/health-node.ts +128 -0
- package/src/n3-force-graph/index.ts +31 -0
- package/src/n3-force-graph/types.ts +94 -0
- package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
- package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
- package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
- package/src/n4-adaptive-manifold/classifier.ts +139 -0
- package/src/n4-adaptive-manifold/index.ts +45 -0
- package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
- package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
- package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
- package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
- package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
- package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
- package/src/n4-adaptive-manifold/transition.ts +90 -0
- package/src/n4-adaptive-manifold/types.ts +313 -0
- package/src/p0-primitives/index.ts +6 -0
- package/src/p0-primitives/tokens/control-room.css +309 -0
- package/src/p0-primitives/tokens/index.ts +6 -0
- package/src/p0-primitives/tokens/tokens.css +140 -0
- package/src/p0-primitives/tokens/tokens.json +132 -0
- package/src/p0-primitives/types/index.ts +123 -0
- package/src/p0-primitives/types/plugin-types.ts +74 -0
- package/src/p0-primitives/utils/index.ts +39 -0
- package/src/platform/hud-protocol.ts +20 -0
- package/src/platform/index.ts +3 -0
- package/src/s0-state/create-store.ts +84 -0
- package/src/s0-state/derived.ts +41 -0
- package/src/s0-state/index.ts +6 -0
- package/src/showcase/Showcase.svelte +401 -0
- package/src/spatial/BspTilingCanvas.svelte +232 -0
- package/src/spatial/ForceCanvas.svelte +289 -0
- package/src/spatial/LodRenderer.svelte +334 -0
- package/src/spatial/SnapZoomCamera.svelte +185 -0
- package/src/spatial/ZoneTiler.svelte +311 -0
- package/src/spatial/camera-types.ts +45 -0
- package/src/spatial/content-proportional-bsp.ts +253 -0
- package/src/spatial/force-canvas-types.ts +102 -0
- package/src/spatial/index.ts +15 -0
- package/src/spatial/lod-types.ts +70 -0
- package/src/spatial/zone-tiler-types.ts +177 -0
- package/src/stores/vaultStore.svelte +14 -0
- package/src/t0-transport/connection.ts +34 -0
- package/src/t0-transport/error.ts +37 -0
- package/src/t0-transport/events.ts +50 -0
- package/src/t0-transport/index.ts +6 -0
- package/src/t0-transport/invoke.ts +30 -0
- package/src/tauri/index.ts +13 -0
- package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
- package/src/u0-primitives/allay-gauge/index.ts +1 -0
- package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
- package/src/u0-primitives/animated-icon/README.md +53 -0
- package/src/u0-primitives/animated-icon/index.ts +31 -0
- package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
- package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
- package/src/u0-primitives/assay-badge/index.ts +2 -0
- package/src/u0-primitives/avatar/Avatar.svelte +177 -0
- package/src/u0-primitives/badge/Badge.svelte +149 -0
- package/src/u0-primitives/button/Button.svelte +304 -0
- package/src/u0-primitives/card/Card.svelte +182 -0
- package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
- package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
- package/src/u0-primitives/expert-badge/index.ts +1 -0
- package/src/u0-primitives/index.ts +35 -0
- package/src/u0-primitives/input/Input.svelte +237 -0
- package/src/u0-primitives/modal/Modal.svelte +331 -0
- package/src/u0-primitives/modal/index.ts +1 -0
- package/src/u0-primitives/spinner/Spinner.svelte +129 -0
- package/src/u0-primitives/tabs/Tabs.svelte +225 -0
- package/src/u0-primitives/tabs/tabs-types.ts +11 -0
- package/src/u0-primitives/toggle/Toggle.svelte +161 -0
- package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
- package/src/v0-diagrams/DiagramPanel.svelte +68 -0
- package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
- package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
- package/src/v0-diagrams/index.ts +4 -0
- package/src/v0-diagrams/types.ts +28 -0
- package/src/v0-diagrams/v0-tokens.css +6 -0
- package/src/vite-env.d.ts +7 -0
- package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
- package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
- package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
- package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
- package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
- package/src/x0-enchanted-blocks/index.ts +46 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// ForceCanvas — d3-force simulation for a single zone
|
|
3
|
+
// wt-901: Per-zone semantic gravity, wall forces, cell entry velocity
|
|
4
|
+
//
|
|
5
|
+
// Wraps d3-force (not force-graph library) for direct control over forces.
|
|
6
|
+
// Each zone gets an independent simulation. Cells cluster by semantic similarity.
|
|
7
|
+
//
|
|
8
|
+
// S-02: No Math.random — d3-force uses internal PRNG seeded by node index.
|
|
9
|
+
// Integer permille for weights.
|
|
10
|
+
// S-05: Simulation bounded by maxIterations (default 300). alpha decay ensures halt.
|
|
11
|
+
|
|
12
|
+
import { onMount, onDestroy } from 'svelte';
|
|
13
|
+
import type { ForceCell, ForceLink, ForceCanvasConfig, CellPosition } from './force-canvas-types.js';
|
|
14
|
+
import {
|
|
15
|
+
cosineSimilarity,
|
|
16
|
+
semanticSpringLength,
|
|
17
|
+
weightToRadius,
|
|
18
|
+
} from './force-canvas-types.js';
|
|
19
|
+
|
|
20
|
+
// ── Props ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Cells to position in this zone */
|
|
24
|
+
cells: ForceCell[];
|
|
25
|
+
/** Zone width in px */
|
|
26
|
+
width: number;
|
|
27
|
+
/** Zone height in px */
|
|
28
|
+
height: number;
|
|
29
|
+
/** Configuration overrides */
|
|
30
|
+
config?: ForceCanvasConfig;
|
|
31
|
+
/** Whether simulation is active. Default true. Set false for Scelle zones. */
|
|
32
|
+
active?: boolean;
|
|
33
|
+
/** Callback with updated cell positions on each tick */
|
|
34
|
+
onPositionsUpdate?: (positions: CellPosition[]) => void;
|
|
35
|
+
/** Callback when a cell is clicked */
|
|
36
|
+
onCellClick?: (cell: ForceCell) => void;
|
|
37
|
+
/** Monotonic tick counter (passed from parent for deterministic pulse). */
|
|
38
|
+
tick?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
cells,
|
|
43
|
+
width,
|
|
44
|
+
height,
|
|
45
|
+
config,
|
|
46
|
+
active = true,
|
|
47
|
+
onPositionsUpdate,
|
|
48
|
+
onCellClick,
|
|
49
|
+
tick = 0,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
// ── Config defaults ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const chargeStrength = $derived(config?.chargeStrength ?? -80);
|
|
55
|
+
const collisionPadding = $derived(config?.collisionPadding ?? 4);
|
|
56
|
+
const wallMargin = $derived(config?.wallMargin ?? 20);
|
|
57
|
+
const wallStrength = $derived(config?.wallStrength ?? 0.3);
|
|
58
|
+
const minSpringLength = $derived(config?.minSpringLength ?? 30);
|
|
59
|
+
const maxSpringLength = $derived(config?.maxSpringLength ?? 200);
|
|
60
|
+
const maxIterations = $derived(config?.maxIterations ?? 300);
|
|
61
|
+
const velocityDecay = $derived(config?.velocityDecay ?? 0.4);
|
|
62
|
+
const entryVelocity = $derived(config?.entryVelocity ?? 0.1);
|
|
63
|
+
|
|
64
|
+
// ── Simulation state ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
let simulation: any = null; // d3-force simulation
|
|
67
|
+
let positions: CellPosition[] = $state([]);
|
|
68
|
+
|
|
69
|
+
// ── Build semantic links from embeddings ─────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function buildSemanticLinks(cells: ForceCell[]): ForceLink[] {
|
|
72
|
+
const links: ForceLink[] = [];
|
|
73
|
+
const SIMILARITY_THRESHOLD = 0.3;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < cells.length; i++) {
|
|
76
|
+
for (let j = i + 1; j < cells.length; j++) {
|
|
77
|
+
const sim = cosineSimilarity(cells[i].embedding, cells[j].embedding);
|
|
78
|
+
if (sim > SIMILARITY_THRESHOLD) {
|
|
79
|
+
links.push({
|
|
80
|
+
source: cells[i].id,
|
|
81
|
+
target: cells[j].id,
|
|
82
|
+
distance: 1 - sim,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return links;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Wall force (custom d3 force) ─────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function createWallForce(w: number, h: number, margin: number, strength: number) {
|
|
93
|
+
let nodes: ForceCell[] = [];
|
|
94
|
+
|
|
95
|
+
function force(alpha: number) {
|
|
96
|
+
for (const node of nodes) {
|
|
97
|
+
const x = node.x ?? 0;
|
|
98
|
+
const y = node.y ?? 0;
|
|
99
|
+
const r = weightToRadius(node.weight);
|
|
100
|
+
|
|
101
|
+
// Left wall
|
|
102
|
+
if (x - r < margin) {
|
|
103
|
+
node.vx = (node.vx ?? 0) + (margin - x + r) * strength * alpha;
|
|
104
|
+
}
|
|
105
|
+
// Right wall
|
|
106
|
+
if (x + r > w - margin) {
|
|
107
|
+
node.vx = (node.vx ?? 0) - (x + r - w + margin) * strength * alpha;
|
|
108
|
+
}
|
|
109
|
+
// Top wall
|
|
110
|
+
if (y - r < margin) {
|
|
111
|
+
node.vy = (node.vy ?? 0) + (margin - y + r) * strength * alpha;
|
|
112
|
+
}
|
|
113
|
+
// Bottom wall
|
|
114
|
+
if (y + r > h - margin) {
|
|
115
|
+
node.vy = (node.vy ?? 0) - (y + r - h + margin) * strength * alpha;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
force.initialize = (n: ForceCell[]) => { nodes = n; };
|
|
121
|
+
return force;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Initialize / update simulation ───────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function initSimulation() {
|
|
127
|
+
if (!active || cells.length === 0) {
|
|
128
|
+
positions = [];
|
|
129
|
+
onPositionsUpdate?.([]);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const d3 = await import('d3-force');
|
|
134
|
+
|
|
135
|
+
const links = buildSemanticLinks(cells);
|
|
136
|
+
|
|
137
|
+
// Place new cells near center with slight offset
|
|
138
|
+
const cx = width / 2;
|
|
139
|
+
const cy = height / 2;
|
|
140
|
+
for (const cell of cells) {
|
|
141
|
+
if (cell.x === undefined) {
|
|
142
|
+
cell.x = cx + (cell.index ?? 0) * 3;
|
|
143
|
+
cell.y = cy + (cell.index ?? 0) * 2;
|
|
144
|
+
// Entry velocity: nudge away from center
|
|
145
|
+
if (entryVelocity > 0) {
|
|
146
|
+
const angle = ((cell.index ?? 0) / Math.max(1, cells.length)) * Math.PI * 2;
|
|
147
|
+
cell.vx = Math.cos(angle) * entryVelocity * 100;
|
|
148
|
+
cell.vy = Math.sin(angle) * entryVelocity * 100;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
simulation?.stop();
|
|
154
|
+
|
|
155
|
+
simulation = d3
|
|
156
|
+
.forceSimulation(cells as any)
|
|
157
|
+
.alphaDecay(0.02)
|
|
158
|
+
.velocityDecay(velocityDecay)
|
|
159
|
+
.force(
|
|
160
|
+
'charge',
|
|
161
|
+
d3.forceManyBody().strength(chargeStrength)
|
|
162
|
+
)
|
|
163
|
+
.force(
|
|
164
|
+
'collision',
|
|
165
|
+
d3.forceCollide<ForceCell>()
|
|
166
|
+
.radius((d) => weightToRadius(d.weight) + collisionPadding)
|
|
167
|
+
)
|
|
168
|
+
.force(
|
|
169
|
+
'link',
|
|
170
|
+
d3
|
|
171
|
+
.forceLink(links as any)
|
|
172
|
+
.id((d: any) => d.id)
|
|
173
|
+
.distance((link: any) =>
|
|
174
|
+
semanticSpringLength(1 - link.distance, minSpringLength, maxSpringLength)
|
|
175
|
+
)
|
|
176
|
+
.strength(0.5)
|
|
177
|
+
)
|
|
178
|
+
.force('center', d3.forceCenter(cx, cy).strength(0.03))
|
|
179
|
+
.force('wall', createWallForce(width, height, wallMargin, wallStrength))
|
|
180
|
+
.on('tick', () => {
|
|
181
|
+
positions = cells.map((cell) => ({
|
|
182
|
+
cell,
|
|
183
|
+
x: cell.x ?? 0,
|
|
184
|
+
y: cell.y ?? 0,
|
|
185
|
+
radius: weightToRadius(cell.weight),
|
|
186
|
+
}));
|
|
187
|
+
onPositionsUpdate?.(positions);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// S-05: bound total iterations
|
|
191
|
+
simulation.stop();
|
|
192
|
+
for (let i = 0; i < maxIterations && simulation.alpha() > simulation.alphaMin(); i++) {
|
|
193
|
+
simulation.tick();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// After initial settle, switch to live ticking
|
|
197
|
+
simulation.restart();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
onMount(() => {
|
|
203
|
+
initSimulation();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
onDestroy(() => {
|
|
207
|
+
simulation?.stop();
|
|
208
|
+
simulation = null;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── React to cell changes ────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
let prevCellIds = '';
|
|
214
|
+
$effect(() => {
|
|
215
|
+
const ids = cells.map((c) => c.id).sort().join(',');
|
|
216
|
+
if (ids !== prevCellIds) {
|
|
217
|
+
prevCellIds = ids;
|
|
218
|
+
initSimulation();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// React to zone resize
|
|
223
|
+
$effect(() => {
|
|
224
|
+
if (simulation && (width || height)) {
|
|
225
|
+
simulation.force('center')?.x(width / 2).y(height / 2);
|
|
226
|
+
simulation.force('wall', createWallForce(width, height, wallMargin, wallStrength));
|
|
227
|
+
simulation.alpha(0.3).restart();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── Pin/unpin API ────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export function pinCell(cellId: string, x: number, y: number) {
|
|
234
|
+
const cell = cells.find((c) => c.id === cellId);
|
|
235
|
+
if (cell) {
|
|
236
|
+
cell.fx = x;
|
|
237
|
+
cell.fy = y;
|
|
238
|
+
simulation?.alpha(0.1).restart();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function unpinCell(cellId: string) {
|
|
243
|
+
const cell = cells.find((c) => c.id === cellId);
|
|
244
|
+
if (cell) {
|
|
245
|
+
cell.fx = null;
|
|
246
|
+
cell.fy = null;
|
|
247
|
+
simulation?.alpha(0.3).restart();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function reheat(alpha = 0.5) {
|
|
252
|
+
simulation?.alpha(alpha).restart();
|
|
253
|
+
}
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<!-- ForceCanvas is headless — it computes positions, doesn't render DOM.
|
|
257
|
+
The parent reads positions via onPositionsUpdate and renders cells at those coords.
|
|
258
|
+
This is a div solely for lifecycle binding. -->
|
|
259
|
+
<div class="force-canvas" style:display="contents">
|
|
260
|
+
{#each positions as pos (pos.cell.id)}
|
|
261
|
+
<slot name="cell" {pos}>
|
|
262
|
+
<!-- Default: colored circle at computed position -->
|
|
263
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
264
|
+
<div
|
|
265
|
+
class="force-cell-default"
|
|
266
|
+
style:left="{pos.x - pos.radius}px"
|
|
267
|
+
style:top="{pos.y - pos.radius}px"
|
|
268
|
+
style:width="{pos.radius * 2}px"
|
|
269
|
+
style:height="{pos.radius * 2}px"
|
|
270
|
+
onclick={() => onCellClick?.(pos.cell)}
|
|
271
|
+
></div>
|
|
272
|
+
</slot>
|
|
273
|
+
{/each}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<style>
|
|
277
|
+
.force-cell-default {
|
|
278
|
+
position: absolute;
|
|
279
|
+
border-radius: 50%;
|
|
280
|
+
background: var(--color-accent, #b87333);
|
|
281
|
+
opacity: 0.7;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
transition: opacity 0.1s;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.force-cell-default:hover {
|
|
287
|
+
opacity: 1;
|
|
288
|
+
}
|
|
289
|
+
</style>
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// LodRenderer — 4-level LOD rendering with cross-fade transitions
|
|
3
|
+
// wt-902: The signature visual system — recursive zoom experience
|
|
4
|
+
//
|
|
5
|
+
// Renders each cell at the appropriate LOD level based on current zoom.
|
|
6
|
+
// Manages cross-fade transitions between levels (300ms default).
|
|
7
|
+
// Parent provides render callbacks per level; LodRenderer manages which shows.
|
|
8
|
+
//
|
|
9
|
+
// L0 (>0.8x): Full cell — editable, titlebar, resize handles
|
|
10
|
+
// L1 (0.4x-0.8x): Compact — title + type icon + first-line preview
|
|
11
|
+
// L2 (0.15x-0.4x): Chip — colored circle, 2-letter abbrev
|
|
12
|
+
// L3 (<0.15x): Pixel field — cells dissolve into color mosaic
|
|
13
|
+
//
|
|
14
|
+
// S-02: Transition timing from monotonic tick counter, not wall-clock.
|
|
15
|
+
// S-05: Bounded cell count. No unbounded iteration.
|
|
16
|
+
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { CellPosition } from './force-canvas-types.js';
|
|
19
|
+
import type { LodLevel, LodRendererConfig, CellLodState } from './lod-types.js';
|
|
20
|
+
import { zoomToLod, DEFAULT_LOD_THRESHOLDS, chipAbbrev } from './lod-types.js';
|
|
21
|
+
|
|
22
|
+
// ── Props ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
/** Cell positions from ForceCanvas */
|
|
26
|
+
positions: CellPosition[];
|
|
27
|
+
/** Current camera zoom factor (0–∞, 1.0 = default) */
|
|
28
|
+
zoom: number;
|
|
29
|
+
/** Container width in px */
|
|
30
|
+
width: number;
|
|
31
|
+
/** Container height in px */
|
|
32
|
+
height: number;
|
|
33
|
+
/** Camera pan offset X */
|
|
34
|
+
panX?: number;
|
|
35
|
+
/** Camera pan offset Y */
|
|
36
|
+
panY?: number;
|
|
37
|
+
/** Config overrides */
|
|
38
|
+
config?: LodRendererConfig;
|
|
39
|
+
|
|
40
|
+
// ── Level render callbacks (snippets) ──────────────────────────────
|
|
41
|
+
|
|
42
|
+
/** L0: Full cell editor view */
|
|
43
|
+
renderL0?: Snippet<[CellPosition, LodLevel]>;
|
|
44
|
+
/** L1: Compact card view */
|
|
45
|
+
renderL1?: Snippet<[CellPosition, LodLevel]>;
|
|
46
|
+
/** L2: Dot/chip view */
|
|
47
|
+
renderL2?: Snippet<[CellPosition, LodLevel]>;
|
|
48
|
+
/** L3 renders as a full-zone pixel field — no per-cell callback.
|
|
49
|
+
* Instead, the parent provides an L3 overlay snippet. */
|
|
50
|
+
renderL3?: Snippet<[CellPosition[], number]>; // positions[], zoom
|
|
51
|
+
|
|
52
|
+
/** Click handler — forwarded from individual cells */
|
|
53
|
+
onCellClick?: (cellId: string, level: LodLevel) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
positions,
|
|
58
|
+
zoom,
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
panX = 0,
|
|
62
|
+
panY = 0,
|
|
63
|
+
config,
|
|
64
|
+
renderL0,
|
|
65
|
+
renderL1,
|
|
66
|
+
renderL2,
|
|
67
|
+
renderL3,
|
|
68
|
+
onCellClick,
|
|
69
|
+
}: Props = $props();
|
|
70
|
+
|
|
71
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const thresholds = $derived(config?.thresholds ?? DEFAULT_LOD_THRESHOLDS);
|
|
74
|
+
const crossFadeDuration = $derived(config?.crossFadeDuration ?? 300);
|
|
75
|
+
|
|
76
|
+
// ── Current LOD level ────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const currentLod = $derived(zoomToLod(zoom, thresholds));
|
|
79
|
+
|
|
80
|
+
// ── Per-cell LOD state for cross-fade ────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
// Map<cellId, CellLodState> — tracks transition state per cell
|
|
83
|
+
let lodStates: Map<string, CellLodState> = $state(new Map());
|
|
84
|
+
let animFrameId: number | null = null;
|
|
85
|
+
|
|
86
|
+
// Update LOD states when level changes
|
|
87
|
+
$effect(() => {
|
|
88
|
+
const level = currentLod;
|
|
89
|
+
const now = performance.now();
|
|
90
|
+
let needsAnimation = false;
|
|
91
|
+
|
|
92
|
+
for (const pos of positions) {
|
|
93
|
+
const existing = lodStates.get(pos.cell.id);
|
|
94
|
+
|
|
95
|
+
if (!existing) {
|
|
96
|
+
// New cell — no transition
|
|
97
|
+
lodStates.set(pos.cell.id, {
|
|
98
|
+
cellId: pos.cell.id,
|
|
99
|
+
currentLevel: level,
|
|
100
|
+
previousLevel: null,
|
|
101
|
+
transitionProgress: null,
|
|
102
|
+
transitionStart: null,
|
|
103
|
+
});
|
|
104
|
+
} else if (existing.currentLevel !== level) {
|
|
105
|
+
// Level changed — start cross-fade
|
|
106
|
+
lodStates.set(pos.cell.id, {
|
|
107
|
+
cellId: pos.cell.id,
|
|
108
|
+
currentLevel: level,
|
|
109
|
+
previousLevel: existing.currentLevel,
|
|
110
|
+
transitionProgress: 0,
|
|
111
|
+
transitionStart: now,
|
|
112
|
+
});
|
|
113
|
+
needsAnimation = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Clean up cells that no longer exist
|
|
118
|
+
const activeIds = new Set(positions.map((p) => p.cell.id));
|
|
119
|
+
for (const id of lodStates.keys()) {
|
|
120
|
+
if (!activeIds.has(id)) lodStates.delete(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (needsAnimation) startAnimation();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function startAnimation() {
|
|
127
|
+
if (animFrameId !== null) return;
|
|
128
|
+
animFrameId = requestAnimationFrame(animateTransitions);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function animateTransitions() {
|
|
132
|
+
const now = performance.now();
|
|
133
|
+
let anyActive = false;
|
|
134
|
+
|
|
135
|
+
for (const [id, state] of lodStates) {
|
|
136
|
+
if (state.transitionProgress === null || state.transitionStart === null) continue;
|
|
137
|
+
|
|
138
|
+
const elapsed = now - state.transitionStart;
|
|
139
|
+
const progress = Math.min(1, elapsed / crossFadeDuration);
|
|
140
|
+
|
|
141
|
+
lodStates.set(id, { ...state, transitionProgress: progress });
|
|
142
|
+
|
|
143
|
+
if (progress < 1) {
|
|
144
|
+
anyActive = true;
|
|
145
|
+
} else {
|
|
146
|
+
// Transition complete — clear previous level
|
|
147
|
+
lodStates.set(id, {
|
|
148
|
+
...state,
|
|
149
|
+
previousLevel: null,
|
|
150
|
+
transitionProgress: null,
|
|
151
|
+
transitionStart: null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (anyActive) {
|
|
157
|
+
animFrameId = requestAnimationFrame(animateTransitions);
|
|
158
|
+
} else {
|
|
159
|
+
animFrameId = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function getOpacity(cellId: string, forLevel: 'current' | 'previous'): number {
|
|
166
|
+
const state = lodStates.get(cellId);
|
|
167
|
+
if (!state || state.transitionProgress === null) {
|
|
168
|
+
return forLevel === 'current' ? 1 : 0;
|
|
169
|
+
}
|
|
170
|
+
// ease-out cubic
|
|
171
|
+
const t = state.transitionProgress;
|
|
172
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
173
|
+
return forLevel === 'current' ? eased : 1 - eased;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getLodState(cellId: string): CellLodState | undefined {
|
|
177
|
+
return lodStates.get(cellId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Cleanup ──────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
import { onDestroy } from 'svelte';
|
|
183
|
+
|
|
184
|
+
onDestroy(() => {
|
|
185
|
+
if (animFrameId !== null) cancelAnimationFrame(animFrameId);
|
|
186
|
+
});
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<div
|
|
190
|
+
class="lod-renderer"
|
|
191
|
+
style:width="{width}px"
|
|
192
|
+
style:height="{height}px"
|
|
193
|
+
>
|
|
194
|
+
<div
|
|
195
|
+
class="lod-viewport"
|
|
196
|
+
style:transform="translate({panX}px, {panY}px) scale({zoom})"
|
|
197
|
+
style:transform-origin="0 0"
|
|
198
|
+
>
|
|
199
|
+
{#if currentLod === 3 && renderL3}
|
|
200
|
+
<!-- L3: pixel field — render full-zone overlay -->
|
|
201
|
+
<div class="lod-l3-field" style:width="{width / zoom}px" style:height="{height / zoom}px">
|
|
202
|
+
{@render renderL3(positions, zoom)}
|
|
203
|
+
</div>
|
|
204
|
+
{:else}
|
|
205
|
+
<!-- L0/L1/L2: per-cell rendering -->
|
|
206
|
+
{#each positions as pos (pos.cell.id)}
|
|
207
|
+
{@const state = getLodState(pos.cell.id)}
|
|
208
|
+
{@const isTransitioning = state?.transitionProgress !== null && state?.transitionProgress !== undefined}
|
|
209
|
+
|
|
210
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
211
|
+
<div
|
|
212
|
+
class="lod-cell"
|
|
213
|
+
style:left="{pos.x - pos.radius}px"
|
|
214
|
+
style:top="{pos.y - pos.radius}px"
|
|
215
|
+
onclick={() => onCellClick?.(pos.cell.id, currentLod)}
|
|
216
|
+
data-lod={currentLod}
|
|
217
|
+
data-cell-id={pos.cell.id}
|
|
218
|
+
>
|
|
219
|
+
<!-- Current level -->
|
|
220
|
+
<div
|
|
221
|
+
class="lod-layer lod-current"
|
|
222
|
+
style:opacity={getOpacity(pos.cell.id, 'current')}
|
|
223
|
+
>
|
|
224
|
+
{#if currentLod === 0 && renderL0}
|
|
225
|
+
{@render renderL0(pos, currentLod)}
|
|
226
|
+
{:else if currentLod === 1 && renderL1}
|
|
227
|
+
{@render renderL1(pos, currentLod)}
|
|
228
|
+
{:else if currentLod === 2 && renderL2}
|
|
229
|
+
{@render renderL2(pos, currentLod)}
|
|
230
|
+
{:else}
|
|
231
|
+
<!-- Fallback: chip -->
|
|
232
|
+
<div
|
|
233
|
+
class="lod-chip"
|
|
234
|
+
style:width="{pos.radius * 2}px"
|
|
235
|
+
style:height="{pos.radius * 2}px"
|
|
236
|
+
>
|
|
237
|
+
<span class="lod-chip-label">{chipAbbrev(pos.cell.label)}</span>
|
|
238
|
+
</div>
|
|
239
|
+
{/if}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Previous level (cross-fade out) -->
|
|
243
|
+
{#if isTransitioning && state?.previousLevel !== null}
|
|
244
|
+
<div
|
|
245
|
+
class="lod-layer lod-previous"
|
|
246
|
+
style:opacity={getOpacity(pos.cell.id, 'previous')}
|
|
247
|
+
>
|
|
248
|
+
{#if state.previousLevel === 0 && renderL0}
|
|
249
|
+
{@render renderL0(pos, state.previousLevel)}
|
|
250
|
+
{:else if state.previousLevel === 1 && renderL1}
|
|
251
|
+
{@render renderL1(pos, state.previousLevel)}
|
|
252
|
+
{:else if state.previousLevel === 2 && renderL2}
|
|
253
|
+
{@render renderL2(pos, state.previousLevel)}
|
|
254
|
+
{:else}
|
|
255
|
+
<div
|
|
256
|
+
class="lod-chip"
|
|
257
|
+
style:width="{pos.radius * 2}px"
|
|
258
|
+
style:height="{pos.radius * 2}px"
|
|
259
|
+
>
|
|
260
|
+
<span class="lod-chip-label">{chipAbbrev(pos.cell.label)}</span>
|
|
261
|
+
</div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
264
|
+
{/if}
|
|
265
|
+
</div>
|
|
266
|
+
{/each}
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<style>
|
|
272
|
+
.lod-renderer {
|
|
273
|
+
position: relative;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.lod-viewport {
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: 0;
|
|
280
|
+
left: 0;
|
|
281
|
+
will-change: transform;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.lod-cell {
|
|
285
|
+
position: absolute;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.lod-layer {
|
|
290
|
+
position: absolute;
|
|
291
|
+
top: 0;
|
|
292
|
+
left: 0;
|
|
293
|
+
width: 100%;
|
|
294
|
+
height: 100%;
|
|
295
|
+
will-change: opacity;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.lod-previous {
|
|
299
|
+
pointer-events: none;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ── L2 chip fallback ──────────────────────────────────────────── */
|
|
303
|
+
|
|
304
|
+
.lod-chip {
|
|
305
|
+
border-radius: 50%;
|
|
306
|
+
background: var(--color-accent, #b87333);
|
|
307
|
+
display: flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
justify-content: center;
|
|
310
|
+
opacity: 0.85;
|
|
311
|
+
transition: opacity 0.1s;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.lod-chip:hover {
|
|
315
|
+
opacity: 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.lod-chip-label {
|
|
319
|
+
font-family: var(--font-mono, monospace);
|
|
320
|
+
font-size: 9px;
|
|
321
|
+
font-weight: 600;
|
|
322
|
+
color: var(--color-text-primary, #e8e0d0);
|
|
323
|
+
letter-spacing: 0.5px;
|
|
324
|
+
user-select: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ── L3 field ──────────────────────────────────────────────────── */
|
|
328
|
+
|
|
329
|
+
.lod-l3-field {
|
|
330
|
+
position: absolute;
|
|
331
|
+
top: 0;
|
|
332
|
+
left: 0;
|
|
333
|
+
}
|
|
334
|
+
</style>
|