@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,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ManifoldSnapshot,
|
|
3
|
+
GravitationalConfig,
|
|
4
|
+
AlgorithmProvider,
|
|
5
|
+
LayoutAlgorithm,
|
|
6
|
+
} from '../types.js';
|
|
7
|
+
import type { NodeLayout, LayoutResult } from '../../n1-flow-canvas/types.js';
|
|
8
|
+
import { DEFAULT_GRAVITATIONAL_CONFIG } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Node sizes by kind with support for measured content.
|
|
12
|
+
* Checks node.data.measuredSize first (from Enchanted Blocks),
|
|
13
|
+
* then falls back to fixed sizes by kind.
|
|
14
|
+
*/
|
|
15
|
+
function nodeSize(
|
|
16
|
+
kind: string,
|
|
17
|
+
status: string,
|
|
18
|
+
data?: Record<string, unknown>
|
|
19
|
+
): { w: number; h: number } {
|
|
20
|
+
// Check for measured size from Enchanted Blocks integration
|
|
21
|
+
const measured = data?.measuredSize as { w: number; h: number } | undefined;
|
|
22
|
+
if (measured && measured.w > 0 && measured.h > 0) {
|
|
23
|
+
return measured;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fall back to fixed sizes by kind
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case 'agent': return { w: 52, h: 46 }; // Large hexagon
|
|
29
|
+
case 'task': return { w: 72, h: 48 }; // Compact badge with ID
|
|
30
|
+
case 'signal': return { w: 10, h: 10 };
|
|
31
|
+
default: return { w: 140, h: 32 };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gravitational layout with deterministic spiral placement.
|
|
37
|
+
* Guarantees no overlap by construction using explicit overlap checking.
|
|
38
|
+
*/
|
|
39
|
+
export class GravitationalProvider implements AlgorithmProvider {
|
|
40
|
+
readonly algorithm: LayoutAlgorithm = 'gravitational';
|
|
41
|
+
private config: GravitationalConfig;
|
|
42
|
+
|
|
43
|
+
constructor(config: Partial<GravitationalConfig> = {}) {
|
|
44
|
+
this.config = { ...DEFAULT_GRAVITATIONAL_CONFIG, ...config };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateConfig(config: Partial<GravitationalConfig>): void {
|
|
48
|
+
this.config = { ...this.config, ...config };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async layout(
|
|
52
|
+
snapshot: ManifoldSnapshot,
|
|
53
|
+
canvasWidth: number,
|
|
54
|
+
canvasHeight: number,
|
|
55
|
+
): Promise<LayoutResult> {
|
|
56
|
+
const { nodes, edges } = snapshot;
|
|
57
|
+
if (nodes.length === 0) {
|
|
58
|
+
return { positions: new Map(), bfsOrder: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Layout world: compact to fit viewport
|
|
62
|
+
const layoutDim = Math.max(canvasWidth, canvasHeight) * 1.2;
|
|
63
|
+
const cx = layoutDim / 2;
|
|
64
|
+
const cy = layoutDim / 2;
|
|
65
|
+
|
|
66
|
+
// ── Deterministic spiral layout with guaranteed separation ──────────────
|
|
67
|
+
// Sort nodes: agents first (center), then claimed tasks, then by priority
|
|
68
|
+
const sortedNodes = [...nodes].sort((a, b) => {
|
|
69
|
+
if (a.kind === 'agent' && b.kind !== 'agent') return -1;
|
|
70
|
+
if (a.kind !== 'agent' && b.kind === 'agent') return 1;
|
|
71
|
+
const aClaimed = a.metadata?.claiming_agent ? 1 : 0;
|
|
72
|
+
const bClaimed = b.metadata?.claiming_agent ? 1 : 0;
|
|
73
|
+
if (aClaimed !== bClaimed) return bClaimed - aClaimed;
|
|
74
|
+
const priorityOrder: Record<string, number> = { p0: 0, p1: 1, p2: 2, p3: 3 };
|
|
75
|
+
const aPriority = typeof a.metadata?.priority === 'string' ? a.metadata.priority : '';
|
|
76
|
+
const bPriority = typeof b.metadata?.priority === 'string' ? b.metadata.priority : '';
|
|
77
|
+
return (priorityOrder[aPriority] ?? 4) - (priorityOrder[bPriority] ?? 4);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Place nodes in spiral with guaranteed minimum spacing
|
|
81
|
+
const positions = new Map<string, NodeLayout>();
|
|
82
|
+
const placed: Array<{ x: number; y: number; w: number; h: number }> = [];
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < sortedNodes.length; i++) {
|
|
85
|
+
const node = sortedNodes[i];
|
|
86
|
+
const { w, h } = nodeSize(node.kind, node.status, node.metadata);
|
|
87
|
+
|
|
88
|
+
// Golden angle spiral for even distribution
|
|
89
|
+
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
|
90
|
+
const angle = i * goldenAngle;
|
|
91
|
+
|
|
92
|
+
// Radius grows with sqrt(index) for equal area distribution
|
|
93
|
+
// Start with minimum spacing of 60px between nodes
|
|
94
|
+
const minSpacing = 60;
|
|
95
|
+
const baseRadius = Math.sqrt(i) * minSpacing * 1.5;
|
|
96
|
+
|
|
97
|
+
let x = cx + Math.cos(angle) * baseRadius - w / 2;
|
|
98
|
+
let y = cy + Math.sin(angle) * baseRadius - h / 2;
|
|
99
|
+
|
|
100
|
+
// Check for overlaps with placed nodes and nudge if needed
|
|
101
|
+
let attempts = 0;
|
|
102
|
+
while (attempts < 50) {
|
|
103
|
+
let hasOverlap = false;
|
|
104
|
+
for (const p of placed) {
|
|
105
|
+
const dx = Math.abs((x + w/2) - p.x);
|
|
106
|
+
const dy = Math.abs((y + h/2) - p.y);
|
|
107
|
+
const minDx = (w + p.w) / 2 + minSpacing;
|
|
108
|
+
const minDy = (h + p.h) / 2 + minSpacing;
|
|
109
|
+
if (dx < minDx && dy < minDy) {
|
|
110
|
+
hasOverlap = true;
|
|
111
|
+
// Nudge outward along the angle
|
|
112
|
+
const nudge = minSpacing * (1 + attempts * 0.2);
|
|
113
|
+
x = cx + Math.cos(angle) * (baseRadius + nudge) - w / 2;
|
|
114
|
+
y = cy + Math.sin(angle) * (baseRadius + nudge) - h / 2;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!hasOverlap) break;
|
|
119
|
+
attempts++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clamp to bounds
|
|
123
|
+
x = Math.max(20, Math.min(layoutDim - w - 20, x));
|
|
124
|
+
y = Math.max(20, Math.min(layoutDim - h - 20, y));
|
|
125
|
+
|
|
126
|
+
positions.set(node.id, { id: node.id, x, y, width: w, height: h });
|
|
127
|
+
placed.push({ x: x + w/2, y: y + h/2, w, h });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// BFS order for edge reveal
|
|
131
|
+
const incoming = new Set(edges.map((e) => e.target));
|
|
132
|
+
const roots = nodes.filter((n) => !incoming.has(n.id)).map((n) => n.id);
|
|
133
|
+
const visited = new Set<string>();
|
|
134
|
+
const bfsOrder: string[] = [];
|
|
135
|
+
const queue = [...roots];
|
|
136
|
+
while (queue.length > 0) {
|
|
137
|
+
const id = queue.shift()!;
|
|
138
|
+
if (visited.has(id)) continue;
|
|
139
|
+
visited.add(id);
|
|
140
|
+
bfsOrder.push(id);
|
|
141
|
+
for (const e of edges) {
|
|
142
|
+
if (e.source === id && !visited.has(e.target)) queue.push(e.target);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const n of nodes) {
|
|
146
|
+
if (!visited.has(n.id)) bfsOrder.push(n.id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { positions, bfsOrder };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Classify node into gravitational zone based on kind + metadata */
|
|
153
|
+
private classifyZone(
|
|
154
|
+
kind: string,
|
|
155
|
+
metadata: Record<string, unknown>,
|
|
156
|
+
): 'center' | 'mid' | 'periphery' {
|
|
157
|
+
// Agents: zone depends on dominant rotation axis
|
|
158
|
+
if (kind === 'agent') {
|
|
159
|
+
const sparkByAxis = metadata.spark_by_axis as
|
|
160
|
+
| [number, number, number]
|
|
161
|
+
| undefined;
|
|
162
|
+
if (sparkByAxis) {
|
|
163
|
+
const [i, j, k] = sparkByAxis;
|
|
164
|
+
const max = Math.max(i, j, k);
|
|
165
|
+
// Tie-break: k (relation) beats j (actuation) beats i (accumulation).
|
|
166
|
+
// Intentional: balanced agents (i===j===k) land in 'center' — coordination role.
|
|
167
|
+
if (max === k) return 'center'; // Relation axis dominant → coordination
|
|
168
|
+
if (max === j) return 'mid'; // Actuation axis dominant → worker
|
|
169
|
+
return 'periphery'; // Accumulation axis dominant → epistemic
|
|
170
|
+
}
|
|
171
|
+
return 'mid'; // default for agents without axis data
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Tasks: active tasks (claimed) pull to mid; idle tasks orbit by priority.
|
|
175
|
+
// p0 = critical → center; p1 = high → mid; p2/p3 = background → periphery.
|
|
176
|
+
// This gives the idle Pinnacle a legible priority hierarchy, not a random scatter.
|
|
177
|
+
if (kind === 'task') {
|
|
178
|
+
if (metadata.claiming_agent) return 'mid';
|
|
179
|
+
const priority = metadata.priority as string | undefined;
|
|
180
|
+
if (priority === 'p0') return 'center';
|
|
181
|
+
if (priority === 'p1') return 'mid';
|
|
182
|
+
return 'periphery';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Signals: always middleground
|
|
186
|
+
if (kind === 'signal') return 'mid';
|
|
187
|
+
|
|
188
|
+
// Capabilities, intents: background/deferred
|
|
189
|
+
return 'periphery';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// ManifoldProvider — S³ stereographic projection layout.
|
|
2
|
+
// Pure arithmetic: quaternion orientation → 2D disk coordinates.
|
|
3
|
+
// O(n), deterministic, zero simulation.
|
|
4
|
+
//
|
|
5
|
+
// Agents: projected from actual [w,i,j,k] via isometric rotation + orthographic flatten.
|
|
6
|
+
// Tasks: positioned by rotation_axis metadata, row-packed within axis clusters.
|
|
7
|
+
//
|
|
8
|
+
// S-02: no Math.random, no HashMap — all placement is arithmetic.
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ManifoldSnapshot,
|
|
12
|
+
AlgorithmProvider,
|
|
13
|
+
LayoutAlgorithm,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
import type { NodeLayout, LayoutResult } from '../../n1-flow-canvas/types.js';
|
|
16
|
+
|
|
17
|
+
// ── Node sizes (symbol body only, not flag extent) ──────────────────────────
|
|
18
|
+
|
|
19
|
+
function nodeSize(kind: string, status: string): { w: number; h: number } {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case 'agent': return { w: 54, h: 48 };
|
|
22
|
+
case 'task': return status === 'active' ? { w: 120, h: 32 } : { w: 90, h: 26 };
|
|
23
|
+
case 'signal': return { w: 28, h: 24 };
|
|
24
|
+
default: return { w: 90, h: 26 };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Axis mapping ─────────────────────────────────────────────────────────────
|
|
29
|
+
// i = Accumulation (left), j = Actuation (right), k = Relation (top)
|
|
30
|
+
// These are the canonical axis positions on the projected disk.
|
|
31
|
+
|
|
32
|
+
const AXIS_POSITIONS: Record<string, { x: number; y: number }> = {
|
|
33
|
+
accumulation: { x: -0.6, y: 0.0 }, // i-axis → left
|
|
34
|
+
actuation: { x: 0.6, y: 0.0 }, // j-axis → right
|
|
35
|
+
relation: { x: 0.0, y: -0.6 }, // k-axis → top
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Hybrid axis positions are interpolated between the two axes.
|
|
39
|
+
function hybridPosition(
|
|
40
|
+
axisA: string,
|
|
41
|
+
weightA: number,
|
|
42
|
+
axisB: string,
|
|
43
|
+
weightB: number,
|
|
44
|
+
): { x: number; y: number } {
|
|
45
|
+
const a = AXIS_POSITIONS[axisA] ?? { x: 0, y: 0 };
|
|
46
|
+
const b = AXIS_POSITIONS[axisB] ?? { x: 0, y: 0 };
|
|
47
|
+
return {
|
|
48
|
+
x: a.x * weightA + b.x * weightB,
|
|
49
|
+
y: a.y * weightA + b.y * weightB,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Column-based fallback positions — spread tasks across the disk by TOS lifecycle stage.
|
|
54
|
+
// Used when rotation_axis metadata is not yet set (common during early pipeline stages).
|
|
55
|
+
// Positions are in normalized disk coordinates [-1, 1].
|
|
56
|
+
// Cluster centers are separated so that KiCad flags (~140px wide) don't overlap
|
|
57
|
+
// across clusters. Flags flip direction at x > 55% viewport width, so left-side
|
|
58
|
+
// clusters extend right and right-side clusters extend left — keep same-Y clusters
|
|
59
|
+
// at least 0.45 normalized units apart in X (~180px at 960px canvas width).
|
|
60
|
+
const COLUMN_POSITIONS: Record<string, { x: number; y: number }> = {
|
|
61
|
+
SEED: { x: -0.62, y: -0.28 }, // upper-left — proposing, accumulating
|
|
62
|
+
SPROUT: { x: -0.22, y: -0.70 }, // top-center — growing, active
|
|
63
|
+
WEED: { x: -0.62, y: 0.42 }, // lower-left — being weeded
|
|
64
|
+
GO: { x: 0.65, y: 0.02 }, // right — actuating, running
|
|
65
|
+
CONSULT: { x: 0.52, y: -0.52 }, // upper-right — relation, consulting
|
|
66
|
+
FREEZE: { x: 0.52, y: 0.50 }, // lower-right — blocked, frozen
|
|
67
|
+
COMPOST: { x: 0.00, y: 0.65 }, // bottom — completed
|
|
68
|
+
PRUNE: { x: 0.22, y: 0.65 }, // bottom-right — pruned
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Parse rotation_axis string back to a 2D position.
|
|
72
|
+
// Pure axis: "actuation" → AXIS_POSITIONS["actuation"]
|
|
73
|
+
// Hybrid: "accumulation:0.6,actuation:0.4" → interpolated
|
|
74
|
+
// Null with cluster hint: falls back to column-based spread position.
|
|
75
|
+
function axisToPosition(
|
|
76
|
+
rotationAxis: string | null | undefined,
|
|
77
|
+
cluster?: string | null,
|
|
78
|
+
): { x: number; y: number } {
|
|
79
|
+
if (!rotationAxis) {
|
|
80
|
+
// Fall back to column-based spread so tasks aren't all at center
|
|
81
|
+
return COLUMN_POSITIONS[cluster ?? ''] ?? { x: 0, y: 0.3 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (rotationAxis.includes(',')) {
|
|
85
|
+
// Hybrid format: "axisA:weightA,axisB:weightB"
|
|
86
|
+
const parts = rotationAxis.split(',');
|
|
87
|
+
const [axisA, wA] = parts[0].split(':');
|
|
88
|
+
const [axisB, wB] = parts[1].split(':');
|
|
89
|
+
return hybridPosition(axisA, parseFloat(wA), axisB, parseFloat(wB));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return AXIS_POSITIONS[rotationAxis] ?? { x: 0, y: 0.3 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Agent projection: quaternion [w,i,j,k] → 2D disk ────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// The quaternion imaginary components (i, j, k) form a point inside the unit ball
|
|
98
|
+
// (since |q|=1, we have i²+j²+k² ≤ 1). We apply an isometric rotation to get a
|
|
99
|
+
// good viewing angle, then orthographically project to 2D.
|
|
100
|
+
//
|
|
101
|
+
// Isometric rotation: rotate 45° around Y, then 35.264° around X
|
|
102
|
+
// (the standard isometric viewing angle). This spreads the three axes evenly
|
|
103
|
+
// on the 2D plane.
|
|
104
|
+
|
|
105
|
+
const ISO_ANGLE_Y = Math.PI / 4; // 45°
|
|
106
|
+
const ISO_ANGLE_X = Math.asin(1 / Math.sqrt(3)); // ~35.264°
|
|
107
|
+
|
|
108
|
+
function projectQuaternionToDisk(
|
|
109
|
+
orientation: [number, number, number, number], // [w, i, j, k]
|
|
110
|
+
): { x: number; y: number } {
|
|
111
|
+
const [, qi, qj, qk] = orientation;
|
|
112
|
+
|
|
113
|
+
// Rotate around Y axis
|
|
114
|
+
const cosY = Math.cos(ISO_ANGLE_Y);
|
|
115
|
+
const sinY = Math.sin(ISO_ANGLE_Y);
|
|
116
|
+
const rx = qi * cosY + qk * sinY;
|
|
117
|
+
const ry = qj;
|
|
118
|
+
const rz = -qi * sinY + qk * cosY;
|
|
119
|
+
|
|
120
|
+
// Rotate around X axis
|
|
121
|
+
const cosX = Math.cos(ISO_ANGLE_X);
|
|
122
|
+
const sinX = Math.sin(ISO_ANGLE_X);
|
|
123
|
+
const finalY = ry * cosX - rz * sinX;
|
|
124
|
+
|
|
125
|
+
// Orthographic projection: drop Z
|
|
126
|
+
return { x: rx, y: finalY };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── ManifoldProvider ─────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export class ManifoldProvider implements AlgorithmProvider {
|
|
132
|
+
readonly algorithm: LayoutAlgorithm = 'manifold';
|
|
133
|
+
|
|
134
|
+
async layout(
|
|
135
|
+
snapshot: ManifoldSnapshot,
|
|
136
|
+
canvasWidth: number,
|
|
137
|
+
canvasHeight: number,
|
|
138
|
+
): Promise<LayoutResult> {
|
|
139
|
+
const { nodes, edges } = snapshot;
|
|
140
|
+
if (nodes.length === 0) {
|
|
141
|
+
return { positions: new Map(), bfsOrder: [] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Guard: if canvas hasn't been measured yet, return empty to avoid
|
|
145
|
+
// collapsing all nodes to (0,0).
|
|
146
|
+
if (canvasWidth < 100 || canvasHeight < 100) {
|
|
147
|
+
return { positions: new Map(), bfsOrder: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Asymmetric disk radii — use more horizontal space since canvases are typically wider.
|
|
151
|
+
// X uses 42% of width, Y uses 38% of height. This spreads nodes across the full viewport
|
|
152
|
+
// rather than clustering near center.
|
|
153
|
+
const diskRadiusX = canvasWidth * 0.42;
|
|
154
|
+
const diskRadiusY = canvasHeight * 0.38;
|
|
155
|
+
const centerX = canvasWidth / 2;
|
|
156
|
+
const centerY = canvasHeight / 2;
|
|
157
|
+
|
|
158
|
+
const positions = new Map<string, NodeLayout>();
|
|
159
|
+
|
|
160
|
+
// ── Separate agents and tasks ──────────────────────────────────────────
|
|
161
|
+
const agents = nodes.filter(n => n.kind === 'agent');
|
|
162
|
+
const tasks = nodes.filter(n => n.kind === 'task');
|
|
163
|
+
const others = nodes.filter(n => n.kind !== 'agent' && n.kind !== 'task');
|
|
164
|
+
|
|
165
|
+
// ── Agents: project from quaternion orientation ────────────────────────
|
|
166
|
+
// Agents occupy the inner disk (45% radius) so they never land on top of
|
|
167
|
+
// task clusters, which live on the full outer disk (100% radius).
|
|
168
|
+
const AGENT_DISK_SCALE = 0.45;
|
|
169
|
+
for (const agent of agents) {
|
|
170
|
+
const size = nodeSize(agent.kind, agent.status);
|
|
171
|
+
const proj = projectQuaternionToDisk(
|
|
172
|
+
(agent.metadata.orientation as [number, number, number, number]) ?? [1, 0, 0, 0],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const x = centerX + proj.x * diskRadiusX * AGENT_DISK_SCALE - size.w / 2;
|
|
176
|
+
const y = centerY + proj.y * diskRadiusY * AGENT_DISK_SCALE - size.h / 2;
|
|
177
|
+
|
|
178
|
+
positions.set(agent.id, {
|
|
179
|
+
id: agent.id,
|
|
180
|
+
x,
|
|
181
|
+
y,
|
|
182
|
+
width: size.w,
|
|
183
|
+
height: size.h,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Tasks: position by rotation axis, row-packed within clusters ───────
|
|
188
|
+
// Group tasks by their axis position (normalized to a string key)
|
|
189
|
+
const taskGroups = new Map<string, Array<{ task: typeof tasks[number]; pos: { x: number; y: number } }>>();
|
|
190
|
+
for (const task of tasks) {
|
|
191
|
+
const rotationAxis = task.hint?.rotation_axis as string | null | undefined;
|
|
192
|
+
const cluster = task.hint?.cluster as string | null | undefined;
|
|
193
|
+
const pos = axisToPosition(rotationAxis, cluster);
|
|
194
|
+
const key = `${pos.x.toFixed(2)},${pos.y.toFixed(2)}`;
|
|
195
|
+
if (!taskGroups.has(key)) taskGroups.set(key, []);
|
|
196
|
+
taskGroups.get(key)!.push({ task, pos });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Pack tasks within each cluster: row-major, left-to-right.
|
|
200
|
+
//
|
|
201
|
+
// Padding geometry (close-zoom KiCad flag constraints):
|
|
202
|
+
// Flag extends 118px from node center (tip=8 + body=110).
|
|
203
|
+
// Node half-width = 45px. Next col body starts at TASK_PAD_W - 45px.
|
|
204
|
+
// No-overlap condition: 118 < TASK_PAD_W - 45 → TASK_PAD_W > 163 → use 170.
|
|
205
|
+
// FLAG_H_HIT = 36px. No-overlap: TASK_PAD_H > 36 → use 40.
|
|
206
|
+
//
|
|
207
|
+
// MAX_COLS: uncapped sqrt(N). For large clusters (e.g. 207 SEED tasks) this
|
|
208
|
+
// spreads wide (15 cols × 170px = 2550px), forcing fit-zoom below 0.8 (mid-tier
|
|
209
|
+
// chips). At close zoom only ~6 cols are visible at once — no flag overlap.
|
|
210
|
+
const TASK_PAD_W = 170; // 118px flag extent + 52px margin → 7px gap to next col
|
|
211
|
+
const TASK_PAD_H = 40; // 36px FLAG_H_HIT + 4px gap
|
|
212
|
+
|
|
213
|
+
for (const [key, group] of taskGroups) {
|
|
214
|
+
const MAX_COLS = Math.max(1, Math.ceil(Math.sqrt(group.length)));
|
|
215
|
+
const [cx, cy] = key.split(',').map(Number);
|
|
216
|
+
const clusterCenterX = centerX + cx * diskRadiusX;
|
|
217
|
+
const clusterCenterY = centerY + cy * diskRadiusY;
|
|
218
|
+
|
|
219
|
+
const cols = Math.min(MAX_COLS, group.length);
|
|
220
|
+
const totalWidth = cols * TASK_PAD_W;
|
|
221
|
+
const startX = clusterCenterX - totalWidth / 2;
|
|
222
|
+
const startY = clusterCenterY - TASK_PAD_H / 2;
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < group.length; i++) {
|
|
225
|
+
const { task } = group[i];
|
|
226
|
+
const size = nodeSize(task.kind, task.status);
|
|
227
|
+
const col = i % cols;
|
|
228
|
+
const row = Math.floor(i / cols);
|
|
229
|
+
|
|
230
|
+
const x = startX + col * TASK_PAD_W + (TASK_PAD_W - size.w) / 2;
|
|
231
|
+
const y = startY + row * TASK_PAD_H + (TASK_PAD_H - size.h) / 2;
|
|
232
|
+
|
|
233
|
+
positions.set(task.id, {
|
|
234
|
+
id: task.id,
|
|
235
|
+
x,
|
|
236
|
+
y,
|
|
237
|
+
width: size.w,
|
|
238
|
+
height: size.h,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Others (signals, etc.): place at center ────────────────────────────
|
|
244
|
+
for (const node of others) {
|
|
245
|
+
const size = nodeSize(node.kind, node.status);
|
|
246
|
+
positions.set(node.id, {
|
|
247
|
+
id: node.id,
|
|
248
|
+
x: centerX - size.w / 2,
|
|
249
|
+
y: centerY - size.h / 2,
|
|
250
|
+
width: size.w,
|
|
251
|
+
height: size.h,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── BFS order for edge reveal ──────────────────────────────────────────
|
|
256
|
+
const incoming = new Set(edges.map(e => e.target));
|
|
257
|
+
const roots = nodes.filter(n => !incoming.has(n.id)).map(n => n.id);
|
|
258
|
+
const visited = new Set<string>();
|
|
259
|
+
const bfsOrder: string[] = [];
|
|
260
|
+
const queue = [...roots];
|
|
261
|
+
while (queue.length > 0) {
|
|
262
|
+
const id = queue.shift()!;
|
|
263
|
+
if (visited.has(id)) continue;
|
|
264
|
+
visited.add(id);
|
|
265
|
+
bfsOrder.push(id);
|
|
266
|
+
for (const e of edges) {
|
|
267
|
+
if (e.source === id && !visited.has(e.target)) queue.push(e.target);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const n of nodes) {
|
|
271
|
+
if (!visited.has(n.id)) bfsOrder.push(n.id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { positions, bfsOrder };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// N4 TransitionAnimator — Interpolates between two LayoutResults
|
|
2
|
+
// Uses requestAnimationFrame for smooth 60fps transitions.
|
|
3
|
+
// S-02: deterministic interpolation, no random values.
|
|
4
|
+
|
|
5
|
+
import type { AnimationFrame, EasingFn, NodeLayout } from './types.js';
|
|
6
|
+
|
|
7
|
+
/** Named easing functions. All map t∈[0,1] → [0,1]. */
|
|
8
|
+
export const EASING_FUNCTIONS: Record<string, EasingFn> = {
|
|
9
|
+
linear: (t) => t,
|
|
10
|
+
easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),
|
|
11
|
+
easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
|
|
12
|
+
spring: (t) => {
|
|
13
|
+
// Damped spring approximation — overshoots ~6% then settles
|
|
14
|
+
const c4 = (2 * Math.PI) / 3;
|
|
15
|
+
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class TransitionAnimator {
|
|
20
|
+
private rafId: number | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Animate from `from` positions to `to` positions over `durationMs`.
|
|
24
|
+
* Calls `onFrame` on each animation frame with the interpolated moment.
|
|
25
|
+
* Calls `onComplete` when the animation finishes.
|
|
26
|
+
*
|
|
27
|
+
* Nodes present in `to` but not `from` appear at their target position (no lerp).
|
|
28
|
+
* Nodes present in `from` but not `to` are dropped from the interpolated map.
|
|
29
|
+
*/
|
|
30
|
+
animate(
|
|
31
|
+
from: Map<string, NodeLayout>,
|
|
32
|
+
to: Map<string, NodeLayout>,
|
|
33
|
+
durationMs: number,
|
|
34
|
+
easing: EasingFn,
|
|
35
|
+
onFrame: (frame: AnimationFrame) => void,
|
|
36
|
+
onComplete: () => void,
|
|
37
|
+
): void {
|
|
38
|
+
this.cancel();
|
|
39
|
+
|
|
40
|
+
const startTime = performance.now();
|
|
41
|
+
|
|
42
|
+
const tick = (now: number) => {
|
|
43
|
+
const elapsed = now - startTime;
|
|
44
|
+
const rawProgress = Math.min(elapsed / durationMs, 1);
|
|
45
|
+
const progress = easing(rawProgress);
|
|
46
|
+
|
|
47
|
+
const positions = new Map<string, NodeLayout>();
|
|
48
|
+
|
|
49
|
+
for (const [id, target] of to) {
|
|
50
|
+
const source = from.get(id);
|
|
51
|
+
if (!source) {
|
|
52
|
+
// New node — appear at target position
|
|
53
|
+
positions.set(id, { ...target });
|
|
54
|
+
} else {
|
|
55
|
+
// Interpolate
|
|
56
|
+
positions.set(id, {
|
|
57
|
+
id,
|
|
58
|
+
x: source.x + (target.x - source.x) * progress,
|
|
59
|
+
y: source.y + (target.y - source.y) * progress,
|
|
60
|
+
width: source.width + (target.width - source.width) * progress,
|
|
61
|
+
height: source.height + (target.height - source.height) * progress,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onFrame({ positions, progress, elapsedMs: elapsed });
|
|
67
|
+
|
|
68
|
+
if (rawProgress < 1) {
|
|
69
|
+
this.rafId = requestAnimationFrame(tick);
|
|
70
|
+
} else {
|
|
71
|
+
this.rafId = null;
|
|
72
|
+
onComplete();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.rafId = requestAnimationFrame(tick);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Cancel any in-flight animation. */
|
|
80
|
+
cancel(): void {
|
|
81
|
+
if (this.rafId !== null) {
|
|
82
|
+
cancelAnimationFrame(this.rafId);
|
|
83
|
+
this.rafId = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get active(): boolean {
|
|
88
|
+
return this.rafId !== null;
|
|
89
|
+
}
|
|
90
|
+
}
|