@neoloopy/cld-canvas 0.1.1 → 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/LICENSE +21 -0
- package/README.md +321 -76
- package/package.json +21 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neoloopy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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)
|
|
30
|
+
|
|
31
|
+
Drop the [`<CldCanvas>` component](#the-cldcanvas-component) below into your
|
|
32
|
+
project, hand it a spec, and you're done:
|
|
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
|
+
```
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
32
62
|
|
|
33
|
-
|
|
63
|
+
### The `<CldCanvas>` component
|
|
34
64
|
|
|
35
|
-
|
|
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
|
+
}
|
|
135
|
+
|
|
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";
|
|
41
294
|
|
|
42
|
-
const
|
|
43
|
-
const engine = new NativeEngine(storage, parse, {
|
|
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,47 +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
|
-
|
|
362
|
+
Past rendering, `NativeEngine` is a full create/edit/load API for neoloopy
|
|
363
|
+
models.
|
|
121
364
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
interface VaultStorage {
|
|
126
|
-
exists(path: string): Promise<boolean>;
|
|
127
|
-
read(path: string): Promise<string>;
|
|
128
|
-
write(path: string, data: string): Promise<void>;
|
|
129
|
-
remove(path: string): Promise<void>;
|
|
130
|
-
mkdirs(path: string): Promise<void>;
|
|
131
|
-
rmdir(path: string): Promise<void>;
|
|
132
|
-
move(from: string, to: string): Promise<void>;
|
|
133
|
-
list(path: string): Promise<{ files: string[]; folders: string[] }>;
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Use `MemoryStorage` for tests, demos, and server-side transforms. In production,
|
|
138
|
-
adapt this interface to your host filesystem, browser persistence layer.
|
|
139
|
-
|
|
140
|
-
All paths are vault-relative and use `/` separators.
|
|
141
|
-
|
|
142
|
-
### Model Format
|
|
143
|
-
|
|
144
|
-
A model is stored as:
|
|
145
|
-
|
|
146
|
-
- `model.json` for model metadata and viewport
|
|
147
|
-
- `Nodes/*.md` for variable notes with YAML frontmatter and Markdown body
|
|
148
|
-
- `Loops/*.md` for feedback loop notes
|
|
149
|
-
- optional system notes such as `System.md`
|
|
150
|
-
|
|
151
|
-
The engine preserves unknown frontmatter keys so other neoloopy tools can add
|
|
152
|
-
metadata without being clobbered.
|
|
153
|
-
|
|
154
|
-
### Main Engine Methods
|
|
365
|
+
### Engine methods
|
|
155
366
|
|
|
156
367
|
```ts
|
|
157
368
|
const model = await engine.createModel("Model name");
|
|
@@ -179,7 +390,8 @@ await engine.relayout(model.folder);
|
|
|
179
390
|
await engine.setViewport(model.folder, { x: 0, y: 0, zoom: 1 });
|
|
180
391
|
```
|
|
181
392
|
|
|
182
|
-
Bulk build is
|
|
393
|
+
Bulk build (used above) is the fastest path for importing generated or external
|
|
394
|
+
CLD specs:
|
|
183
395
|
|
|
184
396
|
```ts
|
|
185
397
|
await engine.buildModel(model.folder, {
|
|
@@ -195,7 +407,43 @@ await engine.buildModel(model.folder, {
|
|
|
195
407
|
});
|
|
196
408
|
```
|
|
197
409
|
|
|
198
|
-
|
|
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
|
|
199
447
|
|
|
200
448
|
`NativeEngine.export` supports `json`, `mermaid`, and `markdown`.
|
|
201
449
|
|
|
@@ -213,7 +461,7 @@ You can also use the lower-level renderer functions directly:
|
|
|
213
461
|
import { buildMermaid, render } from "@neoloopy/cld-canvas";
|
|
214
462
|
```
|
|
215
463
|
|
|
216
|
-
## Analysis
|
|
464
|
+
## 5. Analysis helpers
|
|
217
465
|
|
|
218
466
|
```ts
|
|
219
467
|
import { LoopGraph, endogeneity } from "@neoloopy/cld-canvas";
|
|
@@ -224,7 +472,7 @@ const metrics = loopGraph.metrics();
|
|
|
224
472
|
const summary = endogeneity(graph.nodes, loops);
|
|
225
473
|
```
|
|
226
474
|
|
|
227
|
-
## What
|
|
475
|
+
## What is exported
|
|
228
476
|
|
|
229
477
|
The package exports the public modules from `src/index.ts`, including:
|
|
230
478
|
|
|
@@ -236,13 +484,10 @@ The package exports the public modules from `src/index.ts`, including:
|
|
|
236
484
|
- Exporters: `render`, `buildMermaid`, `loopNoteKey`
|
|
237
485
|
- Analysis: `endogeneity`
|
|
238
486
|
|
|
239
|
-
## Notes for
|
|
487
|
+
## Notes for integrators
|
|
240
488
|
|
|
241
489
|
- The package is ESM-only. Use `import`, not `require`.
|
|
242
490
|
- The engine is asynchronous because real storage adapters usually perform I/O.
|
|
243
|
-
- `move` should preserve or update links when your host platform supports it.
|
|
244
|
-
The Obsidian adapter, for example, should route folder moves through Obsidian's
|
|
245
|
-
file manager so wikilinks remain valid.
|
|
246
491
|
- Canvas rendering is stateless. Keep selection, hover, animation clocks, bow
|
|
247
492
|
caches, and badge overrides in your application state and pass them to
|
|
248
493
|
`SceneCache` or `paint`.
|
package/package.json
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neoloopy/cld-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"author": "neoloopy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Framework-agnostic CLD canvas renderer + in-memory edit engine (shared by the neoloopy Obsidian plugin and neoloopy.com).",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cld",
|
|
9
|
+
"causal-loop-diagram",
|
|
10
|
+
"systems-thinking",
|
|
11
|
+
"system-dynamics",
|
|
12
|
+
"feedback-loops",
|
|
13
|
+
"canvas",
|
|
14
|
+
"diagram",
|
|
15
|
+
"visualization",
|
|
16
|
+
"graph",
|
|
17
|
+
"neoloopy"
|
|
18
|
+
],
|
|
7
19
|
"type": "module",
|
|
8
20
|
"main": "dist/index.js",
|
|
9
21
|
"module": "dist/index.js",
|
|
10
22
|
"types": "dist/index.d.ts",
|
|
11
23
|
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
|
|
24
|
+
"sideEffects": false,
|
|
12
25
|
"files": ["dist"],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/frozenabe/neoloopy-obsidian.git",
|
|
29
|
+
"directory": "packages/cld-canvas"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/frozenabe/neoloopy-obsidian/tree/main/packages/cld-canvas#readme",
|
|
32
|
+
"bugs": { "url": "https://github.com/frozenabe/neoloopy-obsidian/issues" },
|
|
13
33
|
"scripts": { "build": "tsc -p tsconfig.json" }
|
|
14
34
|
}
|