@neoloopy/cld-canvas 0.1.2 → 0.1.3
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 +321 -153
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Framework-agnostic causal loop diagram (CLD) canvas renderer and in-memory edit
|
|
4
4
|
engine used by neoloopy surfaces, including the Obsidian plugin and web app.
|
|
5
|
+
Pure TypeScript, ESM-only, with no runtime dependency on Obsidian, React, Vue,
|
|
6
|
+
or a server.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
**This README is ordered easy → hard** — start at the top with the one-tag React
|
|
9
|
+
drop-in and go only as deep as you need:
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
1. [React, in one tag](#1-react-in-one-tag-easiest) — easiest
|
|
12
|
+
2. [Build and render it yourself](#2-build-and-render-it-yourself) — any framework / full control
|
|
13
|
+
3. [The engine](#3-the-engine) — create, edit, and load models
|
|
14
|
+
4. [Exporting](#4-exporting) and [analysis](#5-analysis-helpers) — advanced
|
|
13
15
|
|
|
14
16
|
## Install
|
|
15
17
|
|
|
@@ -17,30 +19,280 @@ Obsidian, React, Vue, or a server. It gives you two pieces:
|
|
|
17
19
|
npm install @neoloopy/cld-canvas
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
If you use `NativeEngine
|
|
21
|
-
package does not bundle one:
|
|
22
|
+
If you use the engine (`NativeEngine`), also install a YAML parser for note
|
|
23
|
+
frontmatter — the package does not bundle one:
|
|
22
24
|
|
|
23
25
|
```sh
|
|
24
26
|
npm install yaml
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
##
|
|
29
|
+
## 1. React, in one tag (easiest)
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- A `CanvasRenderingContext2D` if you use the painter
|
|
31
|
+
Drop the [`<CldCanvas>` component](#the-cldcanvas-component) below into your
|
|
32
|
+
project, hand it a spec, and you're done:
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
```tsx
|
|
35
|
+
import { CldCanvas } from "./CldCanvas";
|
|
36
|
+
|
|
37
|
+
export default function App() {
|
|
38
|
+
return (
|
|
39
|
+
<CldCanvas
|
|
40
|
+
spec={{
|
|
41
|
+
variables: [
|
|
42
|
+
{ label: "Users", type: "stock" },
|
|
43
|
+
{ label: "Word of mouth" },
|
|
44
|
+
{ label: "Signups" },
|
|
45
|
+
],
|
|
46
|
+
links: [
|
|
47
|
+
{ from: "Users", to: "Word of mouth", polarity: "+" },
|
|
48
|
+
{ from: "Word of mouth", to: "Signups", polarity: "+" },
|
|
49
|
+
{ from: "Signups", to: "Users", polarity: "+" },
|
|
50
|
+
],
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
It builds the model, detects the feedback loops, frames the diagram, and gives
|
|
58
|
+
you pan (drag) + zoom (wheel) — with no surrounding CSS required. Props: `spec`
|
|
59
|
+
(required), `theme` (`"light"` | `"dark"` | a custom `Theme`), `interactive`
|
|
60
|
+
(default `true`), `height` (number px or CSS string, default `400`), plus
|
|
61
|
+
`onReady(graph)` / `onError(error)` callbacks.
|
|
62
|
+
|
|
63
|
+
### The `<CldCanvas>` component
|
|
64
|
+
|
|
65
|
+
Copy this one file into your project; its only dependencies are
|
|
66
|
+
`@neoloopy/cld-canvas` and `yaml`.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// CldCanvas.tsx — a drop-in React wrapper around @neoloopy/cld-canvas.
|
|
70
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
71
|
+
import type { CSSProperties } from "react";
|
|
36
72
|
import { parse } from "yaml";
|
|
37
73
|
import {
|
|
38
74
|
MemoryStorage,
|
|
39
75
|
NativeEngine,
|
|
76
|
+
Camera,
|
|
77
|
+
SceneCache,
|
|
78
|
+
paint,
|
|
79
|
+
LIGHT,
|
|
80
|
+
DARK,
|
|
40
81
|
} from "@neoloopy/cld-canvas";
|
|
82
|
+
import type { BuildSpec, GraphView, PaintUi, Theme } from "@neoloopy/cld-canvas";
|
|
83
|
+
|
|
84
|
+
export interface CldCanvasProps {
|
|
85
|
+
/** The causal-loop diagram to render: variables + polarized links. */
|
|
86
|
+
spec: BuildSpec;
|
|
87
|
+
/** Color tokens — "light" / "dark", or a custom Theme. Default "light". */
|
|
88
|
+
theme?: "light" | "dark" | Theme;
|
|
89
|
+
/** Pan (drag) + zoom (wheel). Default true. */
|
|
90
|
+
interactive?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Canvas height. A number is pixels (default 400); a string is used verbatim,
|
|
93
|
+
* so pass "100%" / "60vh" to fill a sized parent. Width always fills the
|
|
94
|
+
* parent, so `<CldCanvas spec={...} />` works with no surrounding CSS.
|
|
95
|
+
*/
|
|
96
|
+
height?: number | string;
|
|
97
|
+
className?: string;
|
|
98
|
+
style?: CSSProperties;
|
|
99
|
+
/** Called with the loaded graph (detected loops, badge labels) after a build. */
|
|
100
|
+
onReady?: (graph: GraphView) => void;
|
|
101
|
+
/** Called if the spec is invalid or the model fails to build. */
|
|
102
|
+
onError?: (error: Error) => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Interaction fields the painter wants every frame; we drive only pan/zoom, so
|
|
106
|
+
// the rest stay inert.
|
|
107
|
+
const PAINT_DEFAULTS = {
|
|
108
|
+
selectedNodeId: null,
|
|
109
|
+
selectedEdgeId: null,
|
|
110
|
+
selectedLoopKey: null,
|
|
111
|
+
liveNodeIds: new Set<string>(),
|
|
112
|
+
linkPreview: null,
|
|
113
|
+
connectNodeId: null,
|
|
114
|
+
loopHighlight: null,
|
|
115
|
+
pulsePhase: 0,
|
|
116
|
+
flowPhase: 0,
|
|
117
|
+
} satisfies Omit<PaintUi, "cssWidth" | "cssHeight" | "dpr">;
|
|
118
|
+
|
|
119
|
+
// The full engine path: an in-memory vault, build the model, load the detected
|
|
120
|
+
// graph. NativeEngine needs a YAML parser for note frontmatter (yaml).
|
|
121
|
+
async function buildGraph(spec: BuildSpec): Promise<GraphView> {
|
|
122
|
+
const engine = new NativeEngine(new MemoryStorage(), parse, {
|
|
123
|
+
modelsRoot: "Models",
|
|
124
|
+
});
|
|
125
|
+
const model = await engine.createModel("model");
|
|
126
|
+
await engine.buildModel(model.folder, spec); // layout defaults to true
|
|
127
|
+
return engine.loadGraph(model.folder);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveTheme(t: CldCanvasProps["theme"]): Theme {
|
|
131
|
+
if (t === "dark") return DARK;
|
|
132
|
+
if (t === "light" || t == null) return LIGHT;
|
|
133
|
+
return t;
|
|
134
|
+
}
|
|
41
135
|
|
|
42
|
-
|
|
43
|
-
|
|
136
|
+
export function CldCanvas({
|
|
137
|
+
spec,
|
|
138
|
+
theme = "light",
|
|
139
|
+
interactive = true,
|
|
140
|
+
height = 400,
|
|
141
|
+
className,
|
|
142
|
+
style,
|
|
143
|
+
onReady,
|
|
144
|
+
onError,
|
|
145
|
+
}: CldCanvasProps) {
|
|
146
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
147
|
+
const camera = useRef(new Camera()).current;
|
|
148
|
+
const sceneCache = useRef(new SceneCache()).current;
|
|
149
|
+
const graphRef = useRef<GraphView | null>(null);
|
|
150
|
+
const fitted = useRef(false);
|
|
151
|
+
const drag = useRef<{ x: number; y: number } | null>(null);
|
|
152
|
+
|
|
153
|
+
const resolved = useMemo(() => resolveTheme(theme), [theme]);
|
|
154
|
+
|
|
155
|
+
// Mutable mirrors so the build effect and the once-registered listeners read
|
|
156
|
+
// current values without re-subscribing.
|
|
157
|
+
const themeRef = useRef(resolved);
|
|
158
|
+
themeRef.current = resolved;
|
|
159
|
+
const interactiveRef = useRef(interactive);
|
|
160
|
+
interactiveRef.current = interactive;
|
|
161
|
+
const onReadyRef = useRef(onReady);
|
|
162
|
+
onReadyRef.current = onReady;
|
|
163
|
+
const onErrorRef = useRef(onError);
|
|
164
|
+
onErrorRef.current = onError;
|
|
165
|
+
|
|
166
|
+
const render = () => {
|
|
167
|
+
const canvas = canvasRef.current;
|
|
168
|
+
const ctx = canvas?.getContext("2d");
|
|
169
|
+
if (!canvas || !ctx) return;
|
|
170
|
+
|
|
171
|
+
const dpr = window.devicePixelRatio || 1;
|
|
172
|
+
const width = canvas.clientWidth;
|
|
173
|
+
const height = canvas.clientHeight;
|
|
174
|
+
const bw = Math.max(1, Math.floor(width * dpr));
|
|
175
|
+
const bh = Math.max(1, Math.floor(height * dpr));
|
|
176
|
+
if (canvas.width !== bw) canvas.width = bw;
|
|
177
|
+
if (canvas.height !== bh) canvas.height = bh;
|
|
178
|
+
|
|
179
|
+
const scene = sceneCache.build(graphRef.current, new Map(), new Map());
|
|
180
|
+
ctx.clearRect(0, 0, bw, bh);
|
|
181
|
+
if (!scene) return;
|
|
182
|
+
|
|
183
|
+
if (!fitted.current && sceneCache.fit(camera, width, height)) {
|
|
184
|
+
fitted.current = true; // frame once per spec; pan/zoom persists after
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
paint(ctx, scene, camera, themeRef.current, {
|
|
188
|
+
...PAINT_DEFAULTS,
|
|
189
|
+
cssWidth: width,
|
|
190
|
+
cssHeight: height,
|
|
191
|
+
dpr,
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
const renderRef = useRef(render);
|
|
195
|
+
renderRef.current = render;
|
|
196
|
+
|
|
197
|
+
// Rebuild only when the spec's content changes (not on every parent render).
|
|
198
|
+
const specKey = useMemo(() => JSON.stringify(spec), [spec]);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
let cancelled = false;
|
|
201
|
+
buildGraph(spec)
|
|
202
|
+
.then((graph) => {
|
|
203
|
+
if (cancelled) return;
|
|
204
|
+
graphRef.current = graph;
|
|
205
|
+
fitted.current = false;
|
|
206
|
+
renderRef.current();
|
|
207
|
+
onReadyRef.current?.(graph);
|
|
208
|
+
})
|
|
209
|
+
.catch((e) => {
|
|
210
|
+
if (!cancelled) {
|
|
211
|
+
onErrorRef.current?.(e instanceof Error ? e : new Error(String(e)));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return () => {
|
|
215
|
+
cancelled = true;
|
|
216
|
+
};
|
|
217
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
218
|
+
}, [specKey]);
|
|
219
|
+
|
|
220
|
+
// Repaint (no refit) when the theme changes.
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
renderRef.current();
|
|
223
|
+
}, [resolved]);
|
|
224
|
+
|
|
225
|
+
// Register resize + wheel once; both read live state via refs.
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const canvas = canvasRef.current;
|
|
228
|
+
if (!canvas) return;
|
|
229
|
+
|
|
230
|
+
const ro = new ResizeObserver(() => renderRef.current());
|
|
231
|
+
ro.observe(canvas);
|
|
232
|
+
|
|
233
|
+
const onWheel = (e: WheelEvent) => {
|
|
234
|
+
if (!interactiveRef.current) return;
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
camera.zoomAt(e.offsetX, e.offsetY, Math.exp(-e.deltaY * 0.0015));
|
|
237
|
+
renderRef.current();
|
|
238
|
+
};
|
|
239
|
+
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
240
|
+
|
|
241
|
+
return () => {
|
|
242
|
+
ro.disconnect();
|
|
243
|
+
canvas.removeEventListener("wheel", onWheel);
|
|
244
|
+
};
|
|
245
|
+
}, [camera]);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<canvas
|
|
249
|
+
ref={canvasRef}
|
|
250
|
+
className={className}
|
|
251
|
+
style={{
|
|
252
|
+
display: "block",
|
|
253
|
+
width: "100%",
|
|
254
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
255
|
+
touchAction: "none",
|
|
256
|
+
background: resolved.paper,
|
|
257
|
+
cursor: interactive ? "grab" : "default",
|
|
258
|
+
...style,
|
|
259
|
+
}}
|
|
260
|
+
onPointerDown={(e) => {
|
|
261
|
+
if (!interactive) return;
|
|
262
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
263
|
+
drag.current = { x: e.clientX, y: e.clientY };
|
|
264
|
+
}}
|
|
265
|
+
onPointerMove={(e) => {
|
|
266
|
+
if (!drag.current) return;
|
|
267
|
+
camera.panBy(e.clientX - drag.current.x, e.clientY - drag.current.y);
|
|
268
|
+
drag.current = { x: e.clientX, y: e.clientY };
|
|
269
|
+
renderRef.current();
|
|
270
|
+
}}
|
|
271
|
+
onPointerUp={(e) => {
|
|
272
|
+
drag.current = null;
|
|
273
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The same wrapper shape works in Vue, Svelte, or vanilla DOM — only the lifecycle
|
|
281
|
+
hook around `buildGraph` + `paint` changes.
|
|
282
|
+
|
|
283
|
+
## 2. Build and render it yourself
|
|
284
|
+
|
|
285
|
+
Not on React, or want full control? The wrapper above is just two steps: build
|
|
286
|
+
a graph with the engine, then paint it. You need an `ES2020` ESM runtime and a
|
|
287
|
+
`CanvasRenderingContext2D`.
|
|
288
|
+
|
|
289
|
+
**Step 1 — build a model and load its graph:**
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { parse } from "yaml";
|
|
293
|
+
import { MemoryStorage, NativeEngine } from "@neoloopy/cld-canvas";
|
|
294
|
+
|
|
295
|
+
const engine = new NativeEngine(new MemoryStorage(), parse, {
|
|
44
296
|
modelsRoot: "Models",
|
|
45
297
|
});
|
|
46
298
|
|
|
@@ -59,24 +311,13 @@ await engine.buildModel(model.folder, {
|
|
|
59
311
|
],
|
|
60
312
|
});
|
|
61
313
|
|
|
62
|
-
const graph = await engine.loadGraph(model.folder);
|
|
63
|
-
|
|
64
|
-
console.log(graph.nodes.map((node) => node.label));
|
|
65
|
-
console.log(graph.loops.length);
|
|
314
|
+
const graph = await engine.loadGraph(model.folder); // detects R/B loops
|
|
66
315
|
```
|
|
67
316
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
The renderer is intentionally UI-framework neutral. Build a renderable scene
|
|
71
|
-
from a `GraphView`, configure a `Camera`, then call `paint`.
|
|
317
|
+
**Step 2 — paint the graph to a canvas:**
|
|
72
318
|
|
|
73
319
|
```ts
|
|
74
|
-
import {
|
|
75
|
-
Camera,
|
|
76
|
-
LIGHT,
|
|
77
|
-
SceneCache,
|
|
78
|
-
paint,
|
|
79
|
-
} from "@neoloopy/cld-canvas";
|
|
320
|
+
import { Camera, LIGHT, SceneCache, paint } from "@neoloopy/cld-canvas";
|
|
80
321
|
|
|
81
322
|
const canvas = document.querySelector("canvas")!;
|
|
82
323
|
const ctx = canvas.getContext("2d")!;
|
|
@@ -111,124 +352,17 @@ if (scene) {
|
|
|
111
352
|
}
|
|
112
353
|
```
|
|
113
354
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
355
|
+
Wire `Camera.panBy`, `Camera.zoomAt`, `Camera.setScaleAt`, and `Camera.centerOn`
|
|
356
|
+
to your own pointer, wheel, trackpad, minimap, or toolbar controls. Canvas
|
|
357
|
+
rendering is stateless: keep selection, hover, and animation clocks in your app
|
|
358
|
+
state and pass them into `SceneCache` / `paint`.
|
|
117
359
|
|
|
118
|
-
##
|
|
360
|
+
## 3. The engine
|
|
119
361
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
a spec into a rendered diagram:
|
|
362
|
+
Past rendering, `NativeEngine` is a full create/edit/load API for neoloopy
|
|
363
|
+
models.
|
|
123
364
|
|
|
124
|
-
|
|
125
|
-
import { useEffect, useRef } from "react";
|
|
126
|
-
import { parse } from "yaml";
|
|
127
|
-
import {
|
|
128
|
-
MemoryStorage,
|
|
129
|
-
NativeEngine,
|
|
130
|
-
Camera,
|
|
131
|
-
SceneCache,
|
|
132
|
-
paint,
|
|
133
|
-
LIGHT,
|
|
134
|
-
} from "@neoloopy/cld-canvas";
|
|
135
|
-
import type { BuildSpec } from "@neoloopy/cld-canvas";
|
|
136
|
-
|
|
137
|
-
export function CldCanvas({ spec }: { spec: BuildSpec }) {
|
|
138
|
-
const ref = useRef<HTMLCanvasElement>(null);
|
|
139
|
-
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
let alive = true;
|
|
142
|
-
(async () => {
|
|
143
|
-
const engine = new NativeEngine(new MemoryStorage(), parse, {
|
|
144
|
-
modelsRoot: "Models",
|
|
145
|
-
});
|
|
146
|
-
const model = await engine.createModel("model");
|
|
147
|
-
await engine.buildModel(model.folder, spec);
|
|
148
|
-
const graph = await engine.loadGraph(model.folder);
|
|
149
|
-
|
|
150
|
-
const canvas = ref.current;
|
|
151
|
-
const ctx = canvas?.getContext("2d");
|
|
152
|
-
if (!alive || !canvas || !ctx) return;
|
|
153
|
-
|
|
154
|
-
const dpr = window.devicePixelRatio || 1;
|
|
155
|
-
const w = canvas.clientWidth;
|
|
156
|
-
const h = canvas.clientHeight;
|
|
157
|
-
canvas.width = Math.floor(w * dpr);
|
|
158
|
-
canvas.height = Math.floor(h * dpr);
|
|
159
|
-
|
|
160
|
-
const camera = new Camera();
|
|
161
|
-
const sceneCache = new SceneCache();
|
|
162
|
-
const scene = sceneCache.build(graph, new Map(), new Map());
|
|
163
|
-
if (!scene) return;
|
|
164
|
-
sceneCache.fit(camera, w, h);
|
|
165
|
-
|
|
166
|
-
paint(ctx, scene, camera, LIGHT, {
|
|
167
|
-
cssWidth: w,
|
|
168
|
-
cssHeight: h,
|
|
169
|
-
dpr,
|
|
170
|
-
selectedNodeId: null,
|
|
171
|
-
selectedEdgeId: null,
|
|
172
|
-
selectedLoopKey: null,
|
|
173
|
-
liveNodeIds: new Set(),
|
|
174
|
-
linkPreview: null,
|
|
175
|
-
connectNodeId: null,
|
|
176
|
-
loopHighlight: null,
|
|
177
|
-
pulsePhase: 0,
|
|
178
|
-
flowPhase: 0,
|
|
179
|
-
});
|
|
180
|
-
})();
|
|
181
|
-
return () => {
|
|
182
|
-
alive = false;
|
|
183
|
-
};
|
|
184
|
-
}, [spec]);
|
|
185
|
-
|
|
186
|
-
return <canvas ref={ref} style={{ width: "100%", height: 400 }} />;
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
To make it interactive, keep one `Camera` and one `SceneCache` across renders,
|
|
191
|
-
wire `Camera.panBy` / `Camera.zoomAt` to pointer and wheel events, and repaint.
|
|
192
|
-
The same wrapper shape works in Vue, Svelte, or plain DOM — only the lifecycle
|
|
193
|
-
hook changes.
|
|
194
|
-
|
|
195
|
-
## Engine Concepts
|
|
196
|
-
|
|
197
|
-
### Storage
|
|
198
|
-
|
|
199
|
-
`NativeEngine` works against a small filesystem-like `VaultStorage` interface:
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
interface VaultStorage {
|
|
203
|
-
exists(path: string): Promise<boolean>;
|
|
204
|
-
read(path: string): Promise<string>;
|
|
205
|
-
write(path: string, data: string): Promise<void>;
|
|
206
|
-
remove(path: string): Promise<void>;
|
|
207
|
-
mkdirs(path: string): Promise<void>;
|
|
208
|
-
rmdir(path: string): Promise<void>;
|
|
209
|
-
move(from: string, to: string): Promise<void>;
|
|
210
|
-
list(path: string): Promise<{ files: string[]; folders: string[] }>;
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Use `MemoryStorage` for tests, demos, and server-side transforms. In production,
|
|
215
|
-
adapt this interface to your host filesystem, browser persistence layer.
|
|
216
|
-
|
|
217
|
-
All paths are vault-relative and use `/` separators.
|
|
218
|
-
|
|
219
|
-
### Model Format
|
|
220
|
-
|
|
221
|
-
A model is stored as:
|
|
222
|
-
|
|
223
|
-
- `model.json` for model metadata and viewport
|
|
224
|
-
- `Nodes/*.md` for variable notes with YAML frontmatter and Markdown body
|
|
225
|
-
- `Loops/*.md` for feedback loop notes
|
|
226
|
-
- optional system notes such as `System.md`
|
|
227
|
-
|
|
228
|
-
The engine preserves unknown frontmatter keys so other neoloopy tools can add
|
|
229
|
-
metadata without being clobbered.
|
|
230
|
-
|
|
231
|
-
### Main Engine Methods
|
|
365
|
+
### Engine methods
|
|
232
366
|
|
|
233
367
|
```ts
|
|
234
368
|
const model = await engine.createModel("Model name");
|
|
@@ -256,7 +390,8 @@ await engine.relayout(model.folder);
|
|
|
256
390
|
await engine.setViewport(model.folder, { x: 0, y: 0, zoom: 1 });
|
|
257
391
|
```
|
|
258
392
|
|
|
259
|
-
Bulk build is
|
|
393
|
+
Bulk build (used above) is the fastest path for importing generated or external
|
|
394
|
+
CLD specs:
|
|
260
395
|
|
|
261
396
|
```ts
|
|
262
397
|
await engine.buildModel(model.folder, {
|
|
@@ -272,7 +407,43 @@ await engine.buildModel(model.folder, {
|
|
|
272
407
|
});
|
|
273
408
|
```
|
|
274
409
|
|
|
275
|
-
|
|
410
|
+
### Storage
|
|
411
|
+
|
|
412
|
+
`NativeEngine` works against a small filesystem-like `VaultStorage` interface:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
interface VaultStorage {
|
|
416
|
+
exists(path: string): Promise<boolean>;
|
|
417
|
+
read(path: string): Promise<string>;
|
|
418
|
+
write(path: string, data: string): Promise<void>;
|
|
419
|
+
remove(path: string): Promise<void>;
|
|
420
|
+
mkdirs(path: string): Promise<void>;
|
|
421
|
+
rmdir(path: string): Promise<void>;
|
|
422
|
+
move(from: string, to: string): Promise<void>;
|
|
423
|
+
list(path: string): Promise<{ files: string[]; folders: string[] }>;
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Use `MemoryStorage` for tests, demos, and server-side transforms (it is not
|
|
428
|
+
persistent). In production, adapt this interface to your host filesystem or
|
|
429
|
+
browser persistence layer. All paths are vault-relative and use `/` separators.
|
|
430
|
+
`move` should preserve or update links when your host supports it — the Obsidian
|
|
431
|
+
adapter routes folder moves through Obsidian's file manager so wikilinks remain
|
|
432
|
+
valid.
|
|
433
|
+
|
|
434
|
+
### Model format
|
|
435
|
+
|
|
436
|
+
A model is stored as:
|
|
437
|
+
|
|
438
|
+
- `model.json` for model metadata and viewport
|
|
439
|
+
- `Nodes/*.md` for variable notes with YAML frontmatter and Markdown body
|
|
440
|
+
- `Loops/*.md` for feedback loop notes
|
|
441
|
+
- optional system notes such as `System.md`
|
|
442
|
+
|
|
443
|
+
The engine preserves unknown frontmatter keys so other neoloopy tools can add
|
|
444
|
+
metadata without being clobbered.
|
|
445
|
+
|
|
446
|
+
## 4. Exporting
|
|
276
447
|
|
|
277
448
|
`NativeEngine.export` supports `json`, `mermaid`, and `markdown`.
|
|
278
449
|
|
|
@@ -290,7 +461,7 @@ You can also use the lower-level renderer functions directly:
|
|
|
290
461
|
import { buildMermaid, render } from "@neoloopy/cld-canvas";
|
|
291
462
|
```
|
|
292
463
|
|
|
293
|
-
## Analysis
|
|
464
|
+
## 5. Analysis helpers
|
|
294
465
|
|
|
295
466
|
```ts
|
|
296
467
|
import { LoopGraph, endogeneity } from "@neoloopy/cld-canvas";
|
|
@@ -301,7 +472,7 @@ const metrics = loopGraph.metrics();
|
|
|
301
472
|
const summary = endogeneity(graph.nodes, loops);
|
|
302
473
|
```
|
|
303
474
|
|
|
304
|
-
## What
|
|
475
|
+
## What is exported
|
|
305
476
|
|
|
306
477
|
The package exports the public modules from `src/index.ts`, including:
|
|
307
478
|
|
|
@@ -313,13 +484,10 @@ The package exports the public modules from `src/index.ts`, including:
|
|
|
313
484
|
- Exporters: `render`, `buildMermaid`, `loopNoteKey`
|
|
314
485
|
- Analysis: `endogeneity`
|
|
315
486
|
|
|
316
|
-
## Notes for
|
|
487
|
+
## Notes for integrators
|
|
317
488
|
|
|
318
489
|
- The package is ESM-only. Use `import`, not `require`.
|
|
319
490
|
- The engine is asynchronous because real storage adapters usually perform I/O.
|
|
320
|
-
- `move` should preserve or update links when your host platform supports it.
|
|
321
|
-
The Obsidian adapter, for example, should route folder moves through Obsidian's
|
|
322
|
-
file manager so wikilinks remain valid.
|
|
323
491
|
- Canvas rendering is stateless. Keep selection, hover, animation clocks, bow
|
|
324
492
|
caches, and badge overrides in your application state and pass them to
|
|
325
493
|
`SceneCache` or `paint`.
|
package/package.json
CHANGED