@luispm/zflow-graph 0.1.0 → 0.2.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.
@@ -153,6 +153,31 @@ flow.setNodeTasks(id, [{ text: 'done', done: true }]);
153
153
  flow.setNodeImage(id, 'https://...');
154
154
  ```
155
155
 
156
+ ## Load a graph from your own data model
157
+
158
+ When your app already has `{ nodes, edges }` with string ids — services, components, whatever — use `loadGraph`. It wipes the canvas and inserts everything atomically (one `change` event, one undo snapshot) and resolves edges by your ids:
159
+
160
+ ```js
161
+ flow.loadGraph({
162
+ nodes: [
163
+ { id: 'web', kind: 'process', x: -200, y: 0, title: 'Web' },
164
+ { id: 'gateway', kind: 'process', x: 0, y: 0, title: 'Gateway' },
165
+ { id: 'svc', kind: 'process', x: 200, y: 0, title: 'API',
166
+ data: { repo: 'github.com/acme/api' } },
167
+ ],
168
+ edges: [
169
+ { from: 'web', to: 'gateway' },
170
+ { from: 'gateway', to: 'svc' },
171
+ ],
172
+ });
173
+
174
+ // Find the zflow numeric id from your domain id later — no side table needed.
175
+ flow.findNodeByUserId('svc'); // → some number
176
+ flow.getNodeData(flow.findNodeByUserId('svc')).repo; // → 'github.com/acme/api'
177
+ ```
178
+
179
+ The `id` you provide is preserved through `toJSON()` / `loadJSON()` and remote Yjs edits. `data` is a free-form bag attached to every node for whatever metadata you need to round-trip.
180
+
156
181
  ## Listen to events
157
182
 
158
183
  ```js
@@ -335,6 +335,108 @@ await load();
335
335
 
336
336
  ---
337
337
 
338
+ ## Recipe: load a graph from your own domain model
339
+
340
+ Common case: your app has a `{ nodes, edges }` shape with **string ids** (`'svc_users'`, `'db_main'`, ...). You want to render it, let the user edit, and read it back without ever holding a `logicalId → zid` side table.
341
+
342
+ ```js
343
+ const myGraph = {
344
+ nodes: [
345
+ { id: 'web', kind: 'frontend', x: -200, y: -100, title: 'App web', domain: { repo: 'github.com/acme/web' } },
346
+ { id: 'gateway', kind: 'gateway', x: 0, y: 0, title: 'API gateway' },
347
+ { id: 'svc', kind: 'service', x: 200, y: 0, title: 'Users API' },
348
+ { id: 'db', kind: 'db', x: 400, y: 0, title: 'PostgreSQL' },
349
+ ],
350
+ edges: [
351
+ { from: 'web', to: 'gateway', label: 'HTTPS' },
352
+ { from: 'gateway', to: 'svc' },
353
+ { from: 'svc', to: 'db', label: 'SELECT' },
354
+ ],
355
+ };
356
+
357
+ // Map domain fields into node.data so they survive every operation.
358
+ flow.loadGraph({
359
+ nodes: myGraph.nodes.map(n => ({
360
+ id: n.id, kind: n.kind, x: n.x, y: n.y, title: n.title,
361
+ data: { domain: n.domain ?? null },
362
+ })),
363
+ edges: myGraph.edges,
364
+ });
365
+
366
+ // React to user edits — the runtime gives you back the zflow numeric id,
367
+ // turn it back into your domain id via flow.data.
368
+ flow.on('change', () => {
369
+ const out = { nodes: [], edges: [] };
370
+ for (let i = 0; i < flow.nodeCount(); i++) {
371
+ const d = flow.getNodeData(i) || {};
372
+ const p = flow.getNodePosition(i);
373
+ out.nodes.push({ id: d.__id, kind: flow.kinds[flow.V.kind[i]].name, x: p.x, y: p.y });
374
+ }
375
+ saveToBackend(out);
376
+ });
377
+
378
+ // When the user opens a context-menu action, jump back to your domain id:
379
+ function onContextMenuClick(zid) {
380
+ const myId = flow.getNodeData(zid)?.__id; // e.g. 'svc'
381
+ navigateToServicePage(myId);
382
+ }
383
+ ```
384
+
385
+ After this, the typical "wipe + repopulate" boilerplate every consumer used to write disappears.
386
+
387
+ ---
388
+
389
+ ## Recipe: a custom DOM overlay positioned over a node
390
+
391
+ Want a React/Vue/raw-DOM popover that tracks a node as the user pans/zooms? Use the coordinate helpers and re-position from the `change` event (or on every animation frame for live drag).
392
+
393
+ ```js
394
+ const overlay = document.createElement('div');
395
+ Object.assign(overlay.style, {
396
+ position: 'absolute', pointerEvents: 'none',
397
+ padding: '6px 10px', background: '#161b27', color: '#e6edf3',
398
+ borderRadius: '6px', font: '12px Inter, ui-sans-serif',
399
+ });
400
+ overlay.textContent = 'attached';
401
+ container.appendChild(overlay);
402
+
403
+ const trackedId = 3;
404
+
405
+ function positionOverlay() {
406
+ const p = flow.getNodePosition(trackedId);
407
+ if (!p) { overlay.style.display = 'none'; return; }
408
+ const top = flow.worldToScreen(p.x, p.y - p.h / 2 - 8);
409
+ const dpr = window.devicePixelRatio || 1;
410
+ overlay.style.left = (top.x / dpr) + 'px';
411
+ overlay.style.top = (top.y / dpr) + 'px';
412
+ }
413
+
414
+ // Re-position on commits AND every frame (cheap — just a div transform).
415
+ flow.on('change', positionOverlay);
416
+ function loop() { positionOverlay(); requestAnimationFrame(loop); }
417
+ loop();
418
+ ```
419
+
420
+ ---
421
+
422
+ ## Recipe: atomic edits without flooding listeners
423
+
424
+ A common mistake: 1000 calls to `addNode` fire 1000 `change` events and push 1000 undo snapshots. Use `transaction`:
425
+
426
+ ```js
427
+ flow.transaction(() => {
428
+ for (const row of bigPayload) {
429
+ flow.addNode({ kind: 'service', ...row });
430
+ }
431
+ flow.runAutoLayout();
432
+ });
433
+ // Listeners hear ONE 'change'. The whole batch is ONE undo.
434
+ ```
435
+
436
+ This is what `loadGraph` and `addNodesBulk` do internally.
437
+
438
+ ---
439
+
338
440
  ## Next
339
441
 
340
442
  → [API Reference](./08-api.md) — every method, every event
package/docs/08-api.md CHANGED
@@ -34,12 +34,55 @@ Detach all listeners, remove canvas, release WASM views.
34
34
 
35
35
  ---
36
36
 
37
+ ## Atomic loading
38
+
39
+ ```js
40
+ flow.loadGraph({ nodes, edges }) → Map<userId, zid>
41
+ flow.findNodeByUserId(userId) → number // -1 if not found
42
+ flow.transaction(fn) → result
43
+ ```
44
+
45
+ `loadGraph` wipes the current graph and inserts everything as one atomic operation — single `change` event, single undo snapshot. Edges resolve `from`/`to` against the `id` you put on each node (string, number, anything).
46
+
47
+ **`loadGraph` shape:**
48
+
49
+ ```js
50
+ {
51
+ nodes: [
52
+ { id: 'a', kind: 'service', x: 0, y: 0, title: 'A', data: {...} },
53
+ ],
54
+ edges: [
55
+ { from: 'a', to: 'b', fp: 0, tp: 0, label: 'next' },
56
+ ],
57
+ }
58
+ ```
59
+
60
+ Each node's `id` is also stashed in `node.data.__id`, so `toJSON()` / `loadJSON()` and Yjs sync preserve it. Recover the zflow numeric id from any of:
61
+
62
+ ```js
63
+ const idMap = flow.loadGraph({...});
64
+ idMap.get('a'); // → 0
65
+ flow.findNodeByUserId('a'); // → 0 (works after loadJSON or remote edits)
66
+ ```
67
+
68
+ **`transaction(fn)`** runs `fn` with events and snapshots suspended; emits a single `change` and pushes one snapshot at the end. Safe to nest — only the outermost call commits.
69
+
70
+ ```js
71
+ flow.transaction(() => {
72
+ for (const row of payload) flow.addNode({ kind: 'service', ...row });
73
+ flow.runAutoLayout();
74
+ });
75
+ ```
76
+
77
+ ---
78
+
37
79
  ## Mutation
38
80
 
39
81
  ```js
40
82
  flow.addNode(spec) → number // returns node id or -1
41
83
  flow.addEdge(spec) → number // returns edge id or -1
42
84
  flow.deleteSelection()
85
+ flow.deleteNode(id) // delete one node, keep rest of selection
43
86
  flow.moveNode(id, x, y)
44
87
  flow.duplicateSelection(dx = 40, dy = 40)
45
88
  flow.addNodesBulk(specs) → number[] // bulk insert (returns ids in order)
@@ -54,6 +97,7 @@ flow.addEdgesBulk(specs) → number[]
54
97
  - `title`, `color`, `description`, `tags`, `status`, `progress`
55
98
  - `image`, `checked`, `tasks`, `icon`, `links`
56
99
  - `portIn`, `portOut` — per-node port label override
100
+ - `data` — free-form metadata, round-tripped through serialization
57
101
  - `animate: false` — skip pop-in animation
58
102
 
59
103
  **`addEdge` spec fields:**
@@ -63,11 +107,14 @@ flow.addEdgesBulk(specs) → number[]
63
107
  - `tp: number` — target port index (default 0)
64
108
  - `label: string` — edge label
65
109
 
110
+ > **Compaction & remapping.** When a node is deleted, zflow compacts its internal arrays so the surviving range stays contiguous starting at 0. All JS-side maps (`titles`, `colors`, `data`, `bookmarks`, `breakpoints`, `_values`, metric buffers, edge labels, etc.) are remapped automatically. You **do not** need to maintain external `logicalId → zid` tables — store your id under `data.__id` (or use `loadGraph` which does it for you) and recover it with `findNodeByUserId`.
111
+
66
112
  ---
67
113
 
68
114
  ## Selection
69
115
 
70
116
  ```js
117
+ flow.setSelection([ids]) // replace entire selection
71
118
  flow.setSelected(id, on)
72
119
  flow.toggleSelected(id)
73
120
  flow.clearSelection()
@@ -93,9 +140,12 @@ flow.setNodeChecked(id, bool)
93
140
  flow.setNodeTasks(id, [{text, done}, ...])
94
141
  flow.setNodeIcon(id, glyph)
95
142
  flow.setNodeLinks(id, [{url, label}, ...])
143
+ flow.setNodeData(id, anyObject) // free-form metadata bag
144
+ flow.getNodeData(id) → any
96
145
  flow.setPortInLabels(id, labels)
97
146
  flow.setPortOutLabels(id, labels)
98
147
  flow.setEdgeLabel(eid, label)
148
+ flow.startEditTitle(id) // open the inline title editor
99
149
  ```
100
150
 
101
151
  ---
@@ -126,6 +176,21 @@ flow.runForceLayout(maxFrames = 220)
126
176
 
127
177
  ---
128
178
 
179
+ ## Coordinate space & camera
180
+
181
+ The canvas uses **world space** (the coords you pass to `addNode`) and **screen space** (CSS pixels relative to the container). The pair of helpers converts between them.
182
+
183
+ ```js
184
+ flow.screenToWorld(clientX, clientY) → { x, y } // mouse / pointer event
185
+ flow.worldToScreen(worldX, worldY) → { x, y } // CSS pixels
186
+ flow.getCamera() → { x, y, zoom } // immutable snapshot
187
+ flow.getNodePosition(id) → { x, y, w, h } | null
188
+ ```
189
+
190
+ `screenToWorld` takes raw `clientX/Y` (e.g. from a `MouseEvent`); the helper handles `devicePixelRatio` and the canvas bounding rect internally. `getCamera()` returns a copy — mutating it does not move the camera (use `panTo` / `zoomTo`).
191
+
192
+ ---
193
+
129
194
  ## Locks & read-only
130
195
 
131
196
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luispm/zflow-graph",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "WASM-powered node-edge graph editor + runtime. No framework. 100k nodes at 60fps. Built-in execution engine, Yjs multiplayer, WebGL renderer.",
5
5
  "type": "module",
6
6
  "main": "./dist/zflow.umd.js",
@@ -77,11 +77,11 @@
77
77
  "license": "MIT",
78
78
  "repository": {
79
79
  "type": "git",
80
- "url": "https://github.com/luisg/zflow-graph"
80
+ "url": "https://github.com/LuisPadre25/zflow-graph"
81
81
  },
82
- "homepage": "https://github.com/luisg/zflow-graph#readme",
82
+ "homepage": "https://github.com/LuisPadre25/zflow-graph#readme",
83
83
  "bugs": {
84
- "url": "https://github.com/luisg/zflow-graph/issues"
84
+ "url": "https://github.com/LuisPadre25/zflow-graph/issues"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
@@ -95,6 +95,8 @@
95
95
  "vitest": "^1.6.0"
96
96
  },
97
97
  "peerDependenciesMeta": {
98
- "yjs": { "optional": true }
98
+ "yjs": {
99
+ "optional": true
100
+ }
99
101
  }
100
102
  }