@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,314 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// N2 ManifoldDisk — Poincaré disk projection wrapping n1-flow-canvas
|
|
3
|
+
// κ = 1.0 fixed (standard Poincaré disk — no prop, YAGNI)
|
|
4
|
+
// Shares GraphState with FlowCanvas for instantaneous mode toggle
|
|
5
|
+
//
|
|
6
|
+
// When `agents` is provided, the disk renders live S³ agent orientations
|
|
7
|
+
// (quaternion → Poincaré disk via axis-angle) as glowing points with
|
|
8
|
+
// trailing history. Task topology is drawn behind the agents.
|
|
9
|
+
|
|
10
|
+
import { untrack } from 'svelte';
|
|
11
|
+
import { Canvas, Layer } from 'svelte-canvas';
|
|
12
|
+
import { getArrow } from 'perfect-arrows';
|
|
13
|
+
import type { GraphSnapshot } from '../n1-flow-canvas/types.js';
|
|
14
|
+
import { GraphState } from '../n1-flow-canvas/graph-state.svelte.js';
|
|
15
|
+
import { EDGE_REVEAL_STAGGER_MS } from '../n1-flow-canvas/types.js';
|
|
16
|
+
import { projectToDisk, quaternionToDisk } from './poincare.js';
|
|
17
|
+
|
|
18
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Raw inhabitant wire type — mirrors InhabitantView from ryu/src/stores/manifoldStore. */
|
|
21
|
+
interface InhabitantView {
|
|
22
|
+
id: string;
|
|
23
|
+
orientation: [number, number, number, number]; // [w, i, j, k]
|
|
24
|
+
allay: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
snapshot: GraphSnapshot;
|
|
29
|
+
state: GraphState; // must be the same instance passed to FlowCanvas
|
|
30
|
+
width?: number;
|
|
31
|
+
height?: number;
|
|
32
|
+
selectedId?: string | null; // Visual highlight — does NOT trigger Möbius transform
|
|
33
|
+
/** Raw S³ inhabitant orientations from the manifold store. Projected here at render time. */
|
|
34
|
+
inhabitants?: InhabitantView[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Destructure as `graphStateProp` to avoid conflict with the `$state` rune.
|
|
38
|
+
let { snapshot, state: graphStateProp, width = 800, height = 600, selectedId = null, inhabitants = [] }: Props = $props();
|
|
39
|
+
|
|
40
|
+
// ── Agent trail history ───────────────────────────────────────────────────────
|
|
41
|
+
// Ring buffer of recent disk-projected positions per agent (last TRAIL_LEN frames).
|
|
42
|
+
// Projection happens here — disk radius is known at render time.
|
|
43
|
+
const TRAIL_LEN = 12;
|
|
44
|
+
|
|
45
|
+
// Map<agentId, Array<{u, v}>> — normalized disk coords (×diskRadius at render).
|
|
46
|
+
let agentTrails = $state<Map<string, Array<{ u: number; v: number }>>>(new Map());
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
// Track `inhabitants` as the sole reactive dependency.
|
|
50
|
+
// `agentTrails` is read via untrack so writing it back does NOT re-trigger
|
|
51
|
+
// this effect — avoids the read→write→re-run infinite loop.
|
|
52
|
+
const snap = inhabitants;
|
|
53
|
+
agentTrails = untrack(() => {
|
|
54
|
+
const next = new Map(agentTrails);
|
|
55
|
+
for (const inh of snap) {
|
|
56
|
+
const [w, i, j, k] = inh.orientation;
|
|
57
|
+
const { u, v } = quaternionToDisk(w, i, j, k, 1.0);
|
|
58
|
+
const trail = next.get(inh.id) ?? [];
|
|
59
|
+
trail.push({ u, v });
|
|
60
|
+
if (trail.length > TRAIL_LEN) trail.shift();
|
|
61
|
+
next.set(inh.id, trail);
|
|
62
|
+
}
|
|
63
|
+
for (const id of next.keys()) {
|
|
64
|
+
if (!snap.find(a => a.id === id)) next.delete(id);
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Disk radius = half the smallest dimension, leaving some padding
|
|
71
|
+
const diskRadius = $derived(Math.min(width, height) / 2 - 24);
|
|
72
|
+
const diskCx = $derived(width / 2);
|
|
73
|
+
const diskCy = $derived(height / 2);
|
|
74
|
+
|
|
75
|
+
// Project positions into Poincaré disk space
|
|
76
|
+
const projected = $derived(
|
|
77
|
+
projectToDisk(graphStateProp.positions, graphStateProp.focusCenterId, diskRadius)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ── Edge reveal offsets (re-used from GraphState bfsOrder) ──────────────────
|
|
81
|
+
|
|
82
|
+
let edgeOffsets = $state<Map<string, number>>(new Map());
|
|
83
|
+
|
|
84
|
+
$effect(() => {
|
|
85
|
+
const bfsOrder = graphStateProp.bfsOrder;
|
|
86
|
+
const next = new Map<string, number>();
|
|
87
|
+
for (const e of snapshot.edges) next.set(e.id, 0.0);
|
|
88
|
+
edgeOffsets = next;
|
|
89
|
+
|
|
90
|
+
const nodeIndex = new Map<string, number>();
|
|
91
|
+
bfsOrder.forEach((id, i) => nodeIndex.set(id, i));
|
|
92
|
+
|
|
93
|
+
for (const edge of snapshot.edges) {
|
|
94
|
+
const idx = nodeIndex.get(edge.sourceId) ?? 0;
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
edgeOffsets = new Map(edgeOffsets).set(edge.id, 1.0);
|
|
97
|
+
}, idx * EDGE_REVEAL_STAGGER_MS);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── Design tokens ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const COLORS = {
|
|
104
|
+
bg: '#1a1a1a',
|
|
105
|
+
diskBg: '#111111',
|
|
106
|
+
diskBorder: '#3a3020',
|
|
107
|
+
node: '#2a2a2a',
|
|
108
|
+
nodeBorder: '#b87333',
|
|
109
|
+
nodeText: '#e8e0d0',
|
|
110
|
+
edgeLine: '#b87333',
|
|
111
|
+
edgeFade: '#5a4a30',
|
|
112
|
+
focus: '#d4943a',
|
|
113
|
+
grid: '#1e1e1e',
|
|
114
|
+
selected: '#d4943a',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Column-based node fill colors — matches KOS brass/copper theme.
|
|
118
|
+
// Semi-transparent fills let the dark disk background show through.
|
|
119
|
+
const COLUMN_FILL: Record<string, string> = {
|
|
120
|
+
GO: 'rgba(184, 115, 51, 0.35)', // brass — active
|
|
121
|
+
CONSULT: 'rgba(184, 115, 51, 0.18)', // brass outline — advisory
|
|
122
|
+
WEED: 'rgba(200, 160, 50, 0.22)', // warning amber
|
|
123
|
+
SPROUT: 'rgba(70, 130, 200, 0.20)', // info blue — growing
|
|
124
|
+
FREEZE: 'rgba(80, 110, 180, 0.16)', // cool blue — frozen
|
|
125
|
+
SEED: 'rgba(42, 42, 42, 0.60)', // muted — backlog
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const COLUMN_BORDER: Record<string, string> = {
|
|
129
|
+
GO: '#b87333',
|
|
130
|
+
CONSULT: 'rgba(184, 115, 51, 0.6)',
|
|
131
|
+
WEED: '#c8a032',
|
|
132
|
+
SPROUT: '#4682c8',
|
|
133
|
+
FREEZE: '#506eb4',
|
|
134
|
+
SEED: '#3a3a3a',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const NODE_RADIUS = 4;
|
|
138
|
+
const FONT = '11px var(--font-mono, monospace)';
|
|
139
|
+
|
|
140
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function render({ context }: { context: CanvasRenderingContext2D }) {
|
|
143
|
+
const r = diskRadius;
|
|
144
|
+
const cx = diskCx, cy = diskCy;
|
|
145
|
+
|
|
146
|
+
context.clearRect(0, 0, width, height);
|
|
147
|
+
|
|
148
|
+
// Background
|
|
149
|
+
context.fillStyle = COLORS.bg;
|
|
150
|
+
context.fillRect(0, 0, width, height);
|
|
151
|
+
|
|
152
|
+
// Disk background + clip
|
|
153
|
+
context.save();
|
|
154
|
+
context.beginPath();
|
|
155
|
+
context.arc(cx, cy, r, 0, Math.PI * 2);
|
|
156
|
+
context.fillStyle = COLORS.diskBg;
|
|
157
|
+
context.fill();
|
|
158
|
+
context.strokeStyle = COLORS.diskBorder;
|
|
159
|
+
context.lineWidth = 1;
|
|
160
|
+
context.stroke();
|
|
161
|
+
context.clip();
|
|
162
|
+
|
|
163
|
+
// Subtle geodesic grid lines (3 concentric circles)
|
|
164
|
+
context.strokeStyle = COLORS.grid;
|
|
165
|
+
context.lineWidth = 0.5;
|
|
166
|
+
for (const fraction of [0.33, 0.66, 0.88]) {
|
|
167
|
+
context.beginPath();
|
|
168
|
+
context.arc(cx, cy, r * fraction, 0, Math.PI * 2);
|
|
169
|
+
context.stroke();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Offset all positions to disk center
|
|
173
|
+
function toCanvas(x: number, y: number): [number, number] {
|
|
174
|
+
return [x - diskRadius + cx, y - diskRadius + cy];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Edges ──
|
|
178
|
+
context.font = FONT;
|
|
179
|
+
for (const edge of snapshot.edges) {
|
|
180
|
+
const src = projected.get(edge.sourceId);
|
|
181
|
+
const tgt = projected.get(edge.targetId);
|
|
182
|
+
if (!src || !tgt) continue;
|
|
183
|
+
|
|
184
|
+
const progress = edgeOffsets.get(edge.id) ?? 0;
|
|
185
|
+
if (progress <= 0) continue;
|
|
186
|
+
|
|
187
|
+
const [sx, sy] = toCanvas(src.x + src.width / 2, src.y + src.height / 2);
|
|
188
|
+
const [tx, ty] = toCanvas(tgt.x + tgt.width / 2, tgt.y + tgt.height / 2);
|
|
189
|
+
|
|
190
|
+
const [ax, ay, pcx, pcy, ex, ey, ae] = getArrow(sx, sy, tx, ty, {
|
|
191
|
+
bow: 0.15,
|
|
192
|
+
stretch: 0.3,
|
|
193
|
+
padStart: src.width / 2 + 4,
|
|
194
|
+
padEnd: tgt.width / 2 + 4,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const dx = ex - ax, dy = ey - ay;
|
|
198
|
+
const approxLen = Math.sqrt(dx * dx + dy * dy) * 1.2;
|
|
199
|
+
const dash = approxLen * progress;
|
|
200
|
+
|
|
201
|
+
context.save();
|
|
202
|
+
context.strokeStyle = progress >= 1.0 ? COLORS.edgeLine : COLORS.edgeFade;
|
|
203
|
+
context.lineWidth = 1.5;
|
|
204
|
+
context.setLineDash([dash, approxLen]);
|
|
205
|
+
context.beginPath();
|
|
206
|
+
context.moveTo(ax, ay);
|
|
207
|
+
context.quadraticCurveTo(pcx, pcy, ex, ey);
|
|
208
|
+
context.stroke();
|
|
209
|
+
context.restore();
|
|
210
|
+
|
|
211
|
+
if (progress >= 1.0) {
|
|
212
|
+
context.save();
|
|
213
|
+
context.fillStyle = COLORS.edgeLine;
|
|
214
|
+
context.translate(ex, ey);
|
|
215
|
+
context.rotate(ae);
|
|
216
|
+
context.beginPath();
|
|
217
|
+
context.moveTo(0, 0);
|
|
218
|
+
context.lineTo(-7, -3);
|
|
219
|
+
context.lineTo(-7, 3);
|
|
220
|
+
context.closePath();
|
|
221
|
+
context.fill();
|
|
222
|
+
context.restore();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Nodes ──
|
|
227
|
+
for (const node of snapshot.nodes) {
|
|
228
|
+
const pos = projected.get(node.id);
|
|
229
|
+
if (!pos) continue;
|
|
230
|
+
|
|
231
|
+
const column = (node.data?.column as string | undefined) ?? '';
|
|
232
|
+
const isSelected = node.id === selectedId;
|
|
233
|
+
const [px, py] = toCanvas(pos.x, pos.y);
|
|
234
|
+
const { width: w, height: h } = pos;
|
|
235
|
+
|
|
236
|
+
context.fillStyle = isSelected ? COLORS.selected : (COLUMN_FILL[column] ?? COLORS.node);
|
|
237
|
+
context.strokeStyle = isSelected ? COLORS.selected : (COLUMN_BORDER[column] ?? COLORS.nodeBorder);
|
|
238
|
+
context.lineWidth = isSelected ? 2 : 1;
|
|
239
|
+
|
|
240
|
+
context.beginPath();
|
|
241
|
+
context.roundRect(px, py, w, h, NODE_RADIUS);
|
|
242
|
+
context.fill();
|
|
243
|
+
context.stroke();
|
|
244
|
+
|
|
245
|
+
context.fillStyle = COLORS.nodeText;
|
|
246
|
+
context.font = FONT;
|
|
247
|
+
context.textAlign = 'center';
|
|
248
|
+
context.textBaseline = 'middle';
|
|
249
|
+
const label = node.label.length > 16 ? node.label.slice(0, 14) + '…' : node.label;
|
|
250
|
+
context.fillText(label, px + w / 2, py + h / 2, w - 12);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Inhabitants (S³ orientations) ────────────────────────────────────────
|
|
254
|
+
// Project each inhabitant quaternion to disk coords here (diskRadius known).
|
|
255
|
+
// Trails are in unit-disk space; scale by r to get pixel coords.
|
|
256
|
+
if (inhabitants.length > 0) {
|
|
257
|
+
for (const inh of inhabitants) {
|
|
258
|
+
const [w, i, j, k] = inh.orientation;
|
|
259
|
+
const agent = quaternionToDisk(w, i, j, k, 1.0); // unit coords; scaled by r below
|
|
260
|
+
const trail = agentTrails.get(inh.id) ?? [];
|
|
261
|
+
|
|
262
|
+
// Draw fading trail
|
|
263
|
+
if (trail.length > 1) {
|
|
264
|
+
for (let t = 0; t < trail.length - 1; t++) {
|
|
265
|
+
const alpha = (t / (trail.length - 1)) * 0.5;
|
|
266
|
+
const pt = trail[t];
|
|
267
|
+
const pn = trail[t + 1];
|
|
268
|
+
context.save();
|
|
269
|
+
context.strokeStyle = `rgba(212, 148, 58, ${alpha})`; // brass fade
|
|
270
|
+
context.lineWidth = 1.5;
|
|
271
|
+
context.beginPath();
|
|
272
|
+
context.moveTo(cx + pt.u * r, cy + pt.v * r);
|
|
273
|
+
context.lineTo(cx + pn.u * r, cy + pn.v * r);
|
|
274
|
+
context.stroke();
|
|
275
|
+
context.restore();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Draw glowing agent dot
|
|
280
|
+
const ax = cx + agent.u * r;
|
|
281
|
+
const ay = cy + agent.v * r;
|
|
282
|
+
const dotR = 5;
|
|
283
|
+
|
|
284
|
+
// Outer glow
|
|
285
|
+
const glow = context.createRadialGradient(ax, ay, 0, ax, ay, dotR * 3);
|
|
286
|
+
glow.addColorStop(0, 'rgba(212, 148, 58, 0.6)');
|
|
287
|
+
glow.addColorStop(1, 'rgba(212, 148, 58, 0)');
|
|
288
|
+
context.fillStyle = glow;
|
|
289
|
+
context.beginPath();
|
|
290
|
+
context.arc(ax, ay, dotR * 3, 0, Math.PI * 2);
|
|
291
|
+
context.fill();
|
|
292
|
+
|
|
293
|
+
// Core dot
|
|
294
|
+
context.fillStyle = '#d4943a';
|
|
295
|
+
context.beginPath();
|
|
296
|
+
context.arc(ax, ay, dotR, 0, Math.PI * 2);
|
|
297
|
+
context.fill();
|
|
298
|
+
|
|
299
|
+
// Short ID label
|
|
300
|
+
context.fillStyle = 'rgba(232, 224, 208, 0.85)';
|
|
301
|
+
context.font = '9px var(--font-mono, monospace)';
|
|
302
|
+
context.textAlign = 'center';
|
|
303
|
+
context.textBaseline = 'top';
|
|
304
|
+
context.fillText(inh.id.slice(0, 6), ax, ay + dotR + 3);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
context.restore(); // end clip
|
|
309
|
+
}
|
|
310
|
+
</script>
|
|
311
|
+
|
|
312
|
+
<Canvas {width} {height}>
|
|
313
|
+
<Layer {render} />
|
|
314
|
+
</Canvas>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// N2 Manifold Disk — Poincaré disk projection wrapping n1-flow-canvas
|
|
2
|
+
// κ = 1.0 fixed (standard Poincaré disk, no prop)
|
|
3
|
+
|
|
4
|
+
export { default as ManifoldDisk } from './ManifoldDisk.svelte';
|
|
5
|
+
export { mobiusTransform, projectToDisk, poincareDistance, quaternionToDisk } from './poincare.js';
|
|
6
|
+
export type { GraphStateApi, GraphSnapshot } from '../n1-flow-canvas/types.js';
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Poincaré disk projection — ~50 lines of pure math
|
|
2
|
+
// κ = 1.0 fixed (standard Poincaré disk)
|
|
3
|
+
// S-02: pure functions, no side effects, deterministic
|
|
4
|
+
|
|
5
|
+
import type { NodeLayout } from '../n1-flow-canvas/types.js';
|
|
6
|
+
|
|
7
|
+
// Complex number as [real, imag] tuple
|
|
8
|
+
type Complex = readonly [number, number];
|
|
9
|
+
|
|
10
|
+
// ── Complex arithmetic ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function cadd(a: Complex, b: Complex): Complex {
|
|
13
|
+
return [a[0] + b[0], a[1] + b[1]];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function csub(a: Complex, b: Complex): Complex {
|
|
17
|
+
return [a[0] - b[0], a[1] - b[1]];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cmul(a: Complex, b: Complex): Complex {
|
|
21
|
+
return [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cdiv(a: Complex, b: Complex): Complex {
|
|
25
|
+
const denom = b[0] * b[0] + b[1] * b[1];
|
|
26
|
+
return [(a[0] * b[0] + a[1] * b[1]) / denom, (a[1] * b[0] - a[0] * b[1]) / denom];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cconj(a: Complex): Complex {
|
|
30
|
+
return [a[0], -a[1]];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cabs(a: Complex): number {
|
|
34
|
+
return Math.sqrt(a[0] * a[0] + a[1] * a[1]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Möbius transform ──────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Möbius transform that moves z0 to the origin.
|
|
41
|
+
* f(z) = (z − z0) / (1 − conj(z0) · z)
|
|
42
|
+
* Preserves the unit disk: |z| ≤ 1 → |f(z)| ≤ 1
|
|
43
|
+
*/
|
|
44
|
+
export function mobiusTransform(z: Complex, z0: Complex): Complex {
|
|
45
|
+
const num = csub(z, z0);
|
|
46
|
+
const den = csub([1, 0], cmul(cconj(z0), z));
|
|
47
|
+
return cdiv(num, den);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Project Euclidean node positions onto the Poincaré disk.
|
|
52
|
+
*
|
|
53
|
+
* Steps:
|
|
54
|
+
* 1. Normalize all positions to [-1, 1]² using bounding box
|
|
55
|
+
* 2. Apply Möbius transform to center focusNodeId at origin
|
|
56
|
+
* 3. Scale to diskRadius
|
|
57
|
+
*
|
|
58
|
+
* Returns a new Map — does not mutate input.
|
|
59
|
+
*/
|
|
60
|
+
export function projectToDisk(
|
|
61
|
+
positions: Map<string, NodeLayout>,
|
|
62
|
+
focusNodeId: string | null,
|
|
63
|
+
diskRadius: number,
|
|
64
|
+
): Map<string, { x: number; y: number; width: number; height: number }> {
|
|
65
|
+
if (positions.size === 0) return new Map();
|
|
66
|
+
|
|
67
|
+
// Bounding box
|
|
68
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
69
|
+
for (const p of positions.values()) {
|
|
70
|
+
const cx = p.x + p.width / 2;
|
|
71
|
+
const cy = p.y + p.height / 2;
|
|
72
|
+
if (cx < minX) minX = cx;
|
|
73
|
+
if (cy < minY) minY = cy;
|
|
74
|
+
if (cx > maxX) maxX = cx;
|
|
75
|
+
if (cy > maxY) maxY = cy;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rangeX = maxX - minX || 1;
|
|
79
|
+
const rangeY = maxY - minY || 1;
|
|
80
|
+
const range = Math.max(rangeX, rangeY);
|
|
81
|
+
|
|
82
|
+
// Normalize to Poincaré disk unit coords, then scale to 0.9 max radius
|
|
83
|
+
function toComplex(p: NodeLayout): Complex {
|
|
84
|
+
const cx = p.x + p.width / 2;
|
|
85
|
+
const cy = p.y + p.height / 2;
|
|
86
|
+
return [
|
|
87
|
+
((cx - minX) / range * 2 - rangeX / range) * 0.9,
|
|
88
|
+
((cy - minY) / range * 2 - rangeY / range) * 0.9,
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Focus center in normalized coords
|
|
93
|
+
const focusPos = focusNodeId ? positions.get(focusNodeId) : null;
|
|
94
|
+
const z0: Complex = focusPos ? toComplex(focusPos) : [0, 0];
|
|
95
|
+
|
|
96
|
+
const result = new Map<string, { x: number; y: number; width: number; height: number }>();
|
|
97
|
+
|
|
98
|
+
for (const [id, p] of positions) {
|
|
99
|
+
const z = toComplex(p);
|
|
100
|
+
const projected = mobiusTransform(z, z0);
|
|
101
|
+
// Clamp to unit disk (numerical safety)
|
|
102
|
+
const r = cabs(projected);
|
|
103
|
+
const clamped: Complex = r > 0.99 ? [projected[0] * 0.99 / r, projected[1] * 0.99 / r] : projected;
|
|
104
|
+
|
|
105
|
+
result.set(id, {
|
|
106
|
+
x: clamped[0] * diskRadius + diskRadius - p.width / 2,
|
|
107
|
+
y: clamped[1] * diskRadius + diskRadius - p.height / 2,
|
|
108
|
+
width: p.width,
|
|
109
|
+
height: p.height,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Poincaré distance between two points in the disk.
|
|
118
|
+
* d(u,v) = 2 * atanh(|u - v| / |1 - conj(u)v|)
|
|
119
|
+
*/
|
|
120
|
+
export function poincareDistance(u: Complex, v: Complex): number {
|
|
121
|
+
const num = cabs(csub(u, v));
|
|
122
|
+
const den = cabs(csub([1, 0], cmul(cconj(u), v)));
|
|
123
|
+
return 2 * Math.atanh(num / den);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── S³ quaternion projection ──────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A point on the Poincaré disk derived from a unit quaternion on S³.
|
|
130
|
+
*
|
|
131
|
+
* Fields:
|
|
132
|
+
* - `u`, `v` — disk coordinates in pixels, relative to disk centre
|
|
133
|
+
* - `id` — inhabitant identifier
|
|
134
|
+
* - `allay` — accumulated arc length (monotonically increasing)
|
|
135
|
+
*/
|
|
136
|
+
export interface AgentPosition {
|
|
137
|
+
id: string;
|
|
138
|
+
u: number; // disk x, relative to centre
|
|
139
|
+
v: number; // disk y, relative to centre
|
|
140
|
+
allay: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Project a unit quaternion [w, i, j, k] onto the Poincaré disk.
|
|
145
|
+
*
|
|
146
|
+
* Uses axis-angle parameterisation of S³:
|
|
147
|
+
* θ = 2·acos(clamp(w, −1, 1)) — total rotation angle
|
|
148
|
+
* axis = (i, j, k) / sin(θ/2) — unit rotation axis
|
|
149
|
+
* r = θ/π · 0.95 · diskRadius — radial distance on disk
|
|
150
|
+
* u = r · axis.i — Accumulation axis → disk X
|
|
151
|
+
* v = r · axis.j — Actuation axis → disk Y
|
|
152
|
+
*
|
|
153
|
+
* Properties:
|
|
154
|
+
* - Identity quaternion (w=1) maps to disk centre — zero rotation.
|
|
155
|
+
* - Maximally rotated quaternion (w=−1, θ=2π) maps to boundary.
|
|
156
|
+
* - The rotation axis angle on disk reflects semantic orientation:
|
|
157
|
+
* i=Accumulation (X), j=Actuation (Y), k=Relation (depth, ignored here).
|
|
158
|
+
*
|
|
159
|
+
* S-02: pure function, no side effects.
|
|
160
|
+
*/
|
|
161
|
+
export function quaternionToDisk(
|
|
162
|
+
w: number,
|
|
163
|
+
i: number,
|
|
164
|
+
j: number,
|
|
165
|
+
_k: number,
|
|
166
|
+
diskRadius: number,
|
|
167
|
+
): { u: number; v: number } {
|
|
168
|
+
const wClamped = Math.max(-1, Math.min(1, w));
|
|
169
|
+
const theta = 2 * Math.acos(wClamped);
|
|
170
|
+
|
|
171
|
+
if (theta < 1e-9) {
|
|
172
|
+
// Identity — centre of disk
|
|
173
|
+
return { u: 0, v: 0 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sinHalf = Math.sin(theta / 2);
|
|
177
|
+
// r in [0, 0.95 * diskRadius]; maps θ=0 → centre, θ=π → boundary
|
|
178
|
+
const r = (theta / Math.PI) * 0.95 * diskRadius;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
u: r * (i / sinHalf),
|
|
182
|
+
v: r * (j / sinHalf),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// N3 ForceGraph — Canvas 2D force-directed graph wrapper (vasturiano/force-graph)
|
|
3
|
+
// wt-797: Accepts ForceGraphData, renders with health-aware node/edge callbacks
|
|
4
|
+
//
|
|
5
|
+
// S-02: No Math.random in rendering. Pulse tick is deterministic counter.
|
|
6
|
+
// S-05: Force simulation has cooldownTicks + cooldownTime bounds.
|
|
7
|
+
// Component destroys graph instance on unmount.
|
|
8
|
+
|
|
9
|
+
import { onMount, onDestroy } from 'svelte';
|
|
10
|
+
import type { ForceGraphData, ForceGraphNode, ForceGraphLink, HealthNodeConfig, AnimatedEdgeConfig, TooltipPosition } from './types.js';
|
|
11
|
+
import { createHealthNodeRenderer, createHealthNodeAreaPaint, advancePulseTick } from './health-node.js';
|
|
12
|
+
import { createAnimatedEdgeRenderer } from './animated-edge.js';
|
|
13
|
+
|
|
14
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Graph data: nodes + links */
|
|
18
|
+
data: ForceGraphData;
|
|
19
|
+
/** Container width. Default 800. */
|
|
20
|
+
width?: number;
|
|
21
|
+
/** Container height. Default 600. */
|
|
22
|
+
height?: number;
|
|
23
|
+
/** Health node rendering config */
|
|
24
|
+
nodeConfig?: HealthNodeConfig;
|
|
25
|
+
/** Animated edge rendering config */
|
|
26
|
+
edgeConfig?: AnimatedEdgeConfig;
|
|
27
|
+
/** Background color. Default '#1a1a1a'. */
|
|
28
|
+
backgroundColor?: string;
|
|
29
|
+
/** Callback when a node is clicked */
|
|
30
|
+
onNodeClick?: (node: ForceGraphNode) => void;
|
|
31
|
+
/** Callback when a node is hovered (null = hover left) */
|
|
32
|
+
onNodeHover?: (node: ForceGraphNode | null, position: TooltipPosition) => void;
|
|
33
|
+
/** Callback when a link is clicked */
|
|
34
|
+
onLinkClick?: (link: ForceGraphLink) => void;
|
|
35
|
+
/** Force simulation cooldown ticks. Default 300. S-05: bounded. */
|
|
36
|
+
cooldownTicks?: number;
|
|
37
|
+
/** D3 force charge strength. Default -120. */
|
|
38
|
+
chargeStrength?: number;
|
|
39
|
+
/** D3 force link distance. Default 80. */
|
|
40
|
+
linkDistance?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
data,
|
|
45
|
+
width = 800,
|
|
46
|
+
height = 600,
|
|
47
|
+
nodeConfig,
|
|
48
|
+
edgeConfig,
|
|
49
|
+
backgroundColor = '#1a1a1a',
|
|
50
|
+
onNodeClick,
|
|
51
|
+
onNodeHover,
|
|
52
|
+
onLinkClick,
|
|
53
|
+
cooldownTicks = 300,
|
|
54
|
+
chargeStrength = -120,
|
|
55
|
+
linkDistance = 80,
|
|
56
|
+
}: Props = $props();
|
|
57
|
+
|
|
58
|
+
// ── Internal state ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
let container: HTMLDivElement;
|
|
61
|
+
let graph: any = null; // ForceGraph instance (untyped — library uses chainable API)
|
|
62
|
+
|
|
63
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
onMount(async () => {
|
|
66
|
+
// Dynamic import: force-graph requires DOM
|
|
67
|
+
const ForceGraphLib = (await import('force-graph')).default;
|
|
68
|
+
|
|
69
|
+
// force-graph ships class-style TS types but is actually a factory function
|
|
70
|
+
graph = (ForceGraphLib as unknown as () => (el: HTMLElement) => any)()(container)
|
|
71
|
+
.width(width)
|
|
72
|
+
.height(height)
|
|
73
|
+
.backgroundColor(backgroundColor)
|
|
74
|
+
// Node rendering
|
|
75
|
+
.nodeCanvasObject(createHealthNodeRenderer(nodeConfig))
|
|
76
|
+
.nodeCanvasObjectMode(() => 'replace')
|
|
77
|
+
.nodePointerAreaPaint(createHealthNodeAreaPaint(nodeConfig))
|
|
78
|
+
// Edge rendering
|
|
79
|
+
.linkCanvasObject(createAnimatedEdgeRenderer(edgeConfig))
|
|
80
|
+
.linkCanvasObjectMode(() => 'replace')
|
|
81
|
+
// Link identifiers
|
|
82
|
+
.linkSource('source')
|
|
83
|
+
.linkTarget('target')
|
|
84
|
+
// Force parameters
|
|
85
|
+
.cooldownTicks(cooldownTicks)
|
|
86
|
+
// Interaction
|
|
87
|
+
.onNodeClick((node: ForceGraphNode) => {
|
|
88
|
+
onNodeClick?.(node);
|
|
89
|
+
})
|
|
90
|
+
.onNodeHover((node: ForceGraphNode | null) => {
|
|
91
|
+
// Change cursor
|
|
92
|
+
container.style.cursor = node ? 'pointer' : 'default';
|
|
93
|
+
// Emit hover with position
|
|
94
|
+
if (onNodeHover) {
|
|
95
|
+
const pos: TooltipPosition = node
|
|
96
|
+
? { x: (node.x ?? 0), y: (node.y ?? 0), visible: true }
|
|
97
|
+
: { x: 0, y: 0, visible: false };
|
|
98
|
+
onNodeHover(node, pos);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.onLinkClick((link: ForceGraphLink) => onLinkClick?.(link))
|
|
102
|
+
// Drag to pin
|
|
103
|
+
.onNodeDragEnd((node: ForceGraphNode) => {
|
|
104
|
+
node.fx = node.x;
|
|
105
|
+
node.fy = node.y;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Configure d3 forces after graph creation
|
|
109
|
+
graph.d3Force('charge')?.strength(chargeStrength);
|
|
110
|
+
graph.d3Force('link')?.distance(linkDistance);
|
|
111
|
+
|
|
112
|
+
// Engine tick hook for pulse animation
|
|
113
|
+
graph.onEngineTick(() => advancePulseTick());
|
|
114
|
+
|
|
115
|
+
// Initial data
|
|
116
|
+
graph.graphData({
|
|
117
|
+
nodes: data.nodes,
|
|
118
|
+
links: data.links,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
onDestroy(() => {
|
|
123
|
+
if (graph) {
|
|
124
|
+
graph._destructor();
|
|
125
|
+
graph = null;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── Reactive updates ───────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
$effect(() => {
|
|
132
|
+
if (!graph) return;
|
|
133
|
+
graph.graphData({
|
|
134
|
+
nodes: data.nodes,
|
|
135
|
+
links: data.links,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
$effect(() => {
|
|
140
|
+
if (!graph) return;
|
|
141
|
+
graph.width(width).height(height);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ── Public API (exposed via bind:this on wrapper) ──────────────────────────
|
|
145
|
+
|
|
146
|
+
export function zoomToFit(duration = 400, padding = 40) {
|
|
147
|
+
graph?.zoomToFit(duration, padding);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function centerAt(x: number, y: number, duration = 400) {
|
|
151
|
+
graph?.centerAt(x, y, duration);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function zoom(factor: number, duration = 400) {
|
|
155
|
+
graph?.zoom(factor, duration);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function reheat() {
|
|
159
|
+
graph?.d3ReheatSimulation();
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
bind:this={container}
|
|
165
|
+
class="n3-force-graph"
|
|
166
|
+
style:width="{width}px"
|
|
167
|
+
style:height="{height}px"
|
|
168
|
+
></div>
|
|
169
|
+
|
|
170
|
+
<style>
|
|
171
|
+
.n3-force-graph {
|
|
172
|
+
position: relative;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
border-radius: var(--radius-md, 8px);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.n3-force-graph :global(canvas) {
|
|
178
|
+
border-radius: var(--radius-md, 8px);
|
|
179
|
+
}
|
|
180
|
+
</style>
|