vizcraft 0.2.2 → 1.0.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/CHANGELOG.md +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +104 -2
- package/dist/anim/animationBuilder.d.ts +2 -0
- package/dist/anim/animationBuilder.js +6 -1
- package/dist/anim/spec.d.ts +1 -1
- package/dist/anim/vizcraftAdapter.js +68 -1
- package/dist/builder.d.ts +70 -2
- package/dist/builder.js +719 -118
- package/dist/edgeLabels.d.ts +15 -0
- package/dist/edgeLabels.js +26 -0
- package/dist/edgePaths.d.ts +43 -0
- package/dist/edgePaths.js +253 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/overlayBuilder.d.ts +50 -0
- package/dist/overlayBuilder.js +80 -0
- package/dist/overlays.d.ts +113 -21
- package/dist/overlays.js +319 -1
- package/dist/runtimePatcher.d.ts +3 -3
- package/dist/runtimePatcher.js +231 -31
- package/dist/shapes.d.ts +25 -3
- package/dist/shapes.js +1009 -0
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +24 -0
- package/dist/types.d.ts +207 -1
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/dist/anim/player.test.d.ts +0 -1
- package/dist/anim/player.test.js +0 -49
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -66
package/dist/overlays.js
CHANGED
|
@@ -64,6 +64,7 @@ export const coreGridLabelsOverlay = {
|
|
|
64
64
|
return output;
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
|
+
// ... (OverlayRegistry and other exports remain unchanged) ...
|
|
67
68
|
// Built-in Overlay: Data Points
|
|
68
69
|
export const coreDataPointOverlay = {
|
|
69
70
|
render: ({ spec, nodesById }) => {
|
|
@@ -133,7 +134,324 @@ export const coreDataPointOverlay = {
|
|
|
133
134
|
});
|
|
134
135
|
},
|
|
135
136
|
};
|
|
137
|
+
// Generic Overlay: Rect
|
|
138
|
+
export const coreRectOverlay = {
|
|
139
|
+
render: ({ spec }) => {
|
|
140
|
+
const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
141
|
+
const cls = spec.className ?? 'viz-overlay-rect';
|
|
142
|
+
const rxAttr = rx !== undefined ? ` rx="${rx}"` : '';
|
|
143
|
+
const ryAttr = ry !== undefined ? ` ry="${ry}"` : '';
|
|
144
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
145
|
+
const usingDefaultFill = fill === undefined;
|
|
146
|
+
const usingDefaultStroke = stroke === undefined;
|
|
147
|
+
const resolvedFill = fill ?? '#3b82f6';
|
|
148
|
+
const resolvedStroke = stroke ?? '#3b82f6';
|
|
149
|
+
const resolvedStrokeWidth = strokeWidth ?? 3;
|
|
150
|
+
const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
|
|
151
|
+
const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
|
|
152
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${rxAttr}${ryAttr}${opAttr} class="${cls}" />`;
|
|
153
|
+
},
|
|
154
|
+
update: ({ spec }, container) => {
|
|
155
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
156
|
+
const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
157
|
+
const cls = spec.className ?? 'viz-overlay-rect';
|
|
158
|
+
let rect = container.querySelector('rect');
|
|
159
|
+
if (!rect) {
|
|
160
|
+
rect = document.createElementNS(svgNS, 'rect');
|
|
161
|
+
container.appendChild(rect);
|
|
162
|
+
}
|
|
163
|
+
rect.setAttribute('x', String(x));
|
|
164
|
+
rect.setAttribute('y', String(y));
|
|
165
|
+
rect.setAttribute('width', String(w));
|
|
166
|
+
rect.setAttribute('height', String(h));
|
|
167
|
+
if (fill === undefined) {
|
|
168
|
+
rect.setAttribute('fill', '#3b82f6');
|
|
169
|
+
rect.setAttribute('fill-opacity', '0.12');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
rect.setAttribute('fill', fill);
|
|
173
|
+
rect.removeAttribute('fill-opacity');
|
|
174
|
+
}
|
|
175
|
+
if (stroke === undefined) {
|
|
176
|
+
rect.setAttribute('stroke', '#3b82f6');
|
|
177
|
+
rect.setAttribute('stroke-opacity', '0.9');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rect.setAttribute('stroke', stroke);
|
|
181
|
+
rect.removeAttribute('stroke-opacity');
|
|
182
|
+
}
|
|
183
|
+
rect.setAttribute('stroke-width', String(strokeWidth ?? 3));
|
|
184
|
+
if (rx !== undefined)
|
|
185
|
+
rect.setAttribute('rx', String(rx));
|
|
186
|
+
else
|
|
187
|
+
rect.removeAttribute('rx');
|
|
188
|
+
if (ry !== undefined)
|
|
189
|
+
rect.setAttribute('ry', String(ry));
|
|
190
|
+
else
|
|
191
|
+
rect.removeAttribute('ry');
|
|
192
|
+
if (opacity !== undefined)
|
|
193
|
+
rect.setAttribute('opacity', String(opacity));
|
|
194
|
+
else
|
|
195
|
+
rect.removeAttribute('opacity');
|
|
196
|
+
rect.setAttribute('class', cls);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Generic Overlay: Circle
|
|
200
|
+
export const coreCircleOverlay = {
|
|
201
|
+
render: ({ spec }) => {
|
|
202
|
+
const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
203
|
+
const cls = spec.className ?? 'viz-overlay-circle';
|
|
204
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
205
|
+
const usingDefaultFill = fill === undefined;
|
|
206
|
+
const usingDefaultStroke = stroke === undefined;
|
|
207
|
+
const resolvedFill = fill ?? '#3b82f6';
|
|
208
|
+
const resolvedStroke = stroke ?? '#3b82f6';
|
|
209
|
+
const resolvedStrokeWidth = strokeWidth ?? 3;
|
|
210
|
+
const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
|
|
211
|
+
const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
|
|
212
|
+
return `<circle cx="${x}" cy="${y}" r="${r}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${opAttr} class="${cls}" />`;
|
|
213
|
+
},
|
|
214
|
+
update: ({ spec }, container) => {
|
|
215
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
216
|
+
const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
217
|
+
const cls = spec.className ?? 'viz-overlay-circle';
|
|
218
|
+
let circle = container.querySelector('circle');
|
|
219
|
+
if (!circle) {
|
|
220
|
+
circle = document.createElementNS(svgNS, 'circle');
|
|
221
|
+
container.appendChild(circle);
|
|
222
|
+
}
|
|
223
|
+
circle.setAttribute('cx', String(x));
|
|
224
|
+
circle.setAttribute('cy', String(y));
|
|
225
|
+
circle.setAttribute('r', String(r));
|
|
226
|
+
if (fill === undefined) {
|
|
227
|
+
circle.setAttribute('fill', '#3b82f6');
|
|
228
|
+
circle.setAttribute('fill-opacity', '0.12');
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
circle.setAttribute('fill', fill);
|
|
232
|
+
circle.removeAttribute('fill-opacity');
|
|
233
|
+
}
|
|
234
|
+
if (stroke === undefined) {
|
|
235
|
+
circle.setAttribute('stroke', '#3b82f6');
|
|
236
|
+
circle.setAttribute('stroke-opacity', '0.9');
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
circle.setAttribute('stroke', stroke);
|
|
240
|
+
circle.removeAttribute('stroke-opacity');
|
|
241
|
+
}
|
|
242
|
+
circle.setAttribute('stroke-width', String(strokeWidth ?? 3));
|
|
243
|
+
if (opacity !== undefined)
|
|
244
|
+
circle.setAttribute('opacity', String(opacity));
|
|
245
|
+
else
|
|
246
|
+
circle.removeAttribute('opacity');
|
|
247
|
+
circle.setAttribute('class', cls);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
// Generic Overlay: Text
|
|
251
|
+
export const coreTextOverlay = {
|
|
252
|
+
render: ({ spec }) => {
|
|
253
|
+
const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
|
|
254
|
+
const cls = spec.className ?? 'viz-overlay-text';
|
|
255
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
256
|
+
const fsAttr = fontSize !== undefined ? ` font-size="${fontSize}"` : '';
|
|
257
|
+
const fwAttr = fontWeight !== undefined ? ` font-weight="${fontWeight}"` : '';
|
|
258
|
+
const taAttr = textAnchor !== undefined ? ` text-anchor="${textAnchor}"` : '';
|
|
259
|
+
const dbAttr = dominantBaseline !== undefined
|
|
260
|
+
? ` dominant-baseline="${dominantBaseline}"`
|
|
261
|
+
: '';
|
|
262
|
+
// Basic text rendering; users should avoid untrusted HTML here.
|
|
263
|
+
const resolvedFill = fill ?? '#111';
|
|
264
|
+
return `<text x="${x}" y="${y}" fill="${resolvedFill}"${opAttr}${fsAttr}${fwAttr}${taAttr}${dbAttr} class="${cls}">${text}</text>`;
|
|
265
|
+
},
|
|
266
|
+
update: ({ spec }, container) => {
|
|
267
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
268
|
+
const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
|
|
269
|
+
const cls = spec.className ?? 'viz-overlay-text';
|
|
270
|
+
let el = container.querySelector('text');
|
|
271
|
+
if (!el) {
|
|
272
|
+
el = document.createElementNS(svgNS, 'text');
|
|
273
|
+
container.appendChild(el);
|
|
274
|
+
}
|
|
275
|
+
el.setAttribute('x', String(x));
|
|
276
|
+
el.setAttribute('y', String(y));
|
|
277
|
+
el.setAttribute('fill', fill ?? '#111');
|
|
278
|
+
if (opacity !== undefined)
|
|
279
|
+
el.setAttribute('opacity', String(opacity));
|
|
280
|
+
else
|
|
281
|
+
el.removeAttribute('opacity');
|
|
282
|
+
if (fontSize !== undefined)
|
|
283
|
+
el.setAttribute('font-size', String(fontSize));
|
|
284
|
+
else
|
|
285
|
+
el.removeAttribute('font-size');
|
|
286
|
+
if (fontWeight !== undefined)
|
|
287
|
+
el.setAttribute('font-weight', String(fontWeight));
|
|
288
|
+
else
|
|
289
|
+
el.removeAttribute('font-weight');
|
|
290
|
+
if (textAnchor !== undefined)
|
|
291
|
+
el.setAttribute('text-anchor', textAnchor);
|
|
292
|
+
else
|
|
293
|
+
el.removeAttribute('text-anchor');
|
|
294
|
+
if (dominantBaseline !== undefined)
|
|
295
|
+
el.setAttribute('dominant-baseline', dominantBaseline);
|
|
296
|
+
else
|
|
297
|
+
el.removeAttribute('dominant-baseline');
|
|
298
|
+
el.setAttribute('class', cls);
|
|
299
|
+
el.textContent = text;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
function groupTransform(params) {
|
|
303
|
+
const tx = params.x ?? 0;
|
|
304
|
+
const ty = params.y ?? 0;
|
|
305
|
+
const s = params.scale ?? 1;
|
|
306
|
+
const r = params.rotation ?? 0;
|
|
307
|
+
// translate first so scale/rotation occur around the group origin.
|
|
308
|
+
const parts = [`translate(${tx}, ${ty})`];
|
|
309
|
+
if (r)
|
|
310
|
+
parts.push(`rotate(${r})`);
|
|
311
|
+
if (s !== 1)
|
|
312
|
+
parts.push(`scale(${s})`);
|
|
313
|
+
return parts.join(' ');
|
|
314
|
+
}
|
|
315
|
+
function clamp01(v) {
|
|
316
|
+
if (v < 0)
|
|
317
|
+
return 0;
|
|
318
|
+
if (v > 1)
|
|
319
|
+
return 1;
|
|
320
|
+
return v;
|
|
321
|
+
}
|
|
322
|
+
function effectiveNodePos(node) {
|
|
323
|
+
return {
|
|
324
|
+
x: node.runtime?.x ?? node.pos.x,
|
|
325
|
+
y: node.runtime?.y ?? node.pos.y,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function resolveGroupTransformInputs(params, nodesById) {
|
|
329
|
+
const baseX = params.x ?? 0;
|
|
330
|
+
const baseY = params.y ?? 0;
|
|
331
|
+
let x = baseX;
|
|
332
|
+
let y = baseY;
|
|
333
|
+
if (params.from && params.to) {
|
|
334
|
+
const start = nodesById.get(params.from);
|
|
335
|
+
const end = nodesById.get(params.to);
|
|
336
|
+
if (start && end) {
|
|
337
|
+
const p = clamp01(params.progress ?? 0);
|
|
338
|
+
const a = effectiveNodePos(start);
|
|
339
|
+
const b = effectiveNodePos(end);
|
|
340
|
+
x = a.x + (b.x - a.x) * p + baseX;
|
|
341
|
+
y = a.y + (b.y - a.y) * p + baseY;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const userScale = params.scale ?? 1;
|
|
345
|
+
const m = params.magnitude;
|
|
346
|
+
const magScale = m === undefined ? 1 : 0.85 + 0.3 * clamp01(Math.abs(m));
|
|
347
|
+
const scale = userScale * magScale;
|
|
348
|
+
return {
|
|
349
|
+
x,
|
|
350
|
+
y,
|
|
351
|
+
scale,
|
|
352
|
+
rotation: params.rotation ?? 0,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Composite Overlay: Group
|
|
356
|
+
export const coreGroupOverlay = {
|
|
357
|
+
render: ({ spec, nodesById, edgesById, scene, registry }) => {
|
|
358
|
+
const { children, opacity } = spec.params;
|
|
359
|
+
const inputs = resolveGroupTransformInputs(spec.params, nodesById);
|
|
360
|
+
const tr = groupTransform(inputs);
|
|
361
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
362
|
+
const reg = registry;
|
|
363
|
+
if (!reg) {
|
|
364
|
+
// Best-effort render even if registry is missing.
|
|
365
|
+
return `<g transform="${tr}"${opAttr}></g>`;
|
|
366
|
+
}
|
|
367
|
+
let output = `<g transform="${tr}"${opAttr}>`;
|
|
368
|
+
children.forEach((childSpec, idx) => {
|
|
369
|
+
const renderer = reg.get(childSpec.id);
|
|
370
|
+
if (!renderer)
|
|
371
|
+
return;
|
|
372
|
+
const childCtx = {
|
|
373
|
+
spec: childSpec,
|
|
374
|
+
nodesById,
|
|
375
|
+
edgesById,
|
|
376
|
+
scene,
|
|
377
|
+
registry: reg,
|
|
378
|
+
};
|
|
379
|
+
// Wrap children in their own <g> so update() has stable containers.
|
|
380
|
+
const key = childSpec.key
|
|
381
|
+
? `key:${childSpec.key}`
|
|
382
|
+
: `idx:${idx}:${childSpec.id}`;
|
|
383
|
+
output += `<g data-viz-role="overlay-child" data-overlay-child-id="${key}">`;
|
|
384
|
+
output += renderer.render(childCtx);
|
|
385
|
+
output += '</g>';
|
|
386
|
+
});
|
|
387
|
+
output += '</g>';
|
|
388
|
+
return output;
|
|
389
|
+
},
|
|
390
|
+
update: ({ spec, nodesById, edgesById, scene, registry }, container) => {
|
|
391
|
+
const reg = registry;
|
|
392
|
+
if (!reg)
|
|
393
|
+
return;
|
|
394
|
+
const { children, opacity } = spec.params;
|
|
395
|
+
const inputs = resolveGroupTransformInputs(spec.params, nodesById);
|
|
396
|
+
container.setAttribute('transform', groupTransform(inputs));
|
|
397
|
+
if (opacity !== undefined) {
|
|
398
|
+
container.setAttribute('opacity', String(opacity));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
container.removeAttribute('opacity');
|
|
402
|
+
}
|
|
403
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
404
|
+
const existing = new Map();
|
|
405
|
+
Array.from(container.children).forEach((child) => {
|
|
406
|
+
if (child instanceof SVGGElement) {
|
|
407
|
+
const id = child.getAttribute('data-overlay-child-id');
|
|
408
|
+
if (id)
|
|
409
|
+
existing.set(id, child);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const keep = new Set();
|
|
413
|
+
children.forEach((childSpec, idx) => {
|
|
414
|
+
const renderer = reg.get(childSpec.id);
|
|
415
|
+
if (!renderer)
|
|
416
|
+
return;
|
|
417
|
+
const key = childSpec.key
|
|
418
|
+
? `key:${childSpec.key}`
|
|
419
|
+
: `idx:${idx}:${childSpec.id}`;
|
|
420
|
+
keep.add(key);
|
|
421
|
+
let childGroup = existing.get(key);
|
|
422
|
+
if (!childGroup) {
|
|
423
|
+
childGroup = document.createElementNS(svgNS, 'g');
|
|
424
|
+
childGroup.setAttribute('data-viz-role', 'overlay-child');
|
|
425
|
+
childGroup.setAttribute('data-overlay-child-id', key);
|
|
426
|
+
container.appendChild(childGroup);
|
|
427
|
+
}
|
|
428
|
+
const childCtx = {
|
|
429
|
+
spec: childSpec,
|
|
430
|
+
nodesById,
|
|
431
|
+
edgesById,
|
|
432
|
+
scene,
|
|
433
|
+
registry: reg,
|
|
434
|
+
};
|
|
435
|
+
if (renderer.update) {
|
|
436
|
+
renderer.update(childCtx, childGroup);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
childGroup.innerHTML = renderer.render(childCtx);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
existing.forEach((el, id) => {
|
|
443
|
+
if (!keep.has(id))
|
|
444
|
+
el.remove();
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
};
|
|
136
448
|
export const defaultCoreOverlayRegistry = new CoreOverlayRegistry()
|
|
137
449
|
.register('signal', coreSignalOverlay)
|
|
138
450
|
.register('grid-labels', coreGridLabelsOverlay)
|
|
139
|
-
.register('data-points', coreDataPointOverlay)
|
|
451
|
+
.register('data-points', coreDataPointOverlay)
|
|
452
|
+
// Generic primitives
|
|
453
|
+
.register('rect', coreRectOverlay)
|
|
454
|
+
.register('circle', coreCircleOverlay)
|
|
455
|
+
.register('text', coreTextOverlay)
|
|
456
|
+
// Composite overlays
|
|
457
|
+
.register('group', coreGroupOverlay);
|
package/dist/runtimePatcher.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export interface RuntimePatchCtx {
|
|
|
5
5
|
nodeShapesById: Map<string, SVGElement>;
|
|
6
6
|
nodeLabelsById: Map<string, SVGTextElement>;
|
|
7
7
|
edgeGroupsById: Map<string, SVGGElement>;
|
|
8
|
-
edgeLinesById: Map<string,
|
|
9
|
-
edgeHitsById: Map<string,
|
|
10
|
-
edgeLabelsById: Map<string, SVGTextElement>;
|
|
8
|
+
edgeLinesById: Map<string, SVGPathElement>;
|
|
9
|
+
edgeHitsById: Map<string, SVGPathElement>;
|
|
10
|
+
edgeLabelsById: Map<string, SVGTextElement[]>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createRuntimePatchCtx(svg: SVGSVGElement): RuntimePatchCtx;
|
|
13
13
|
export declare function patchRuntime(scene: VizScene, ctx: RuntimePatchCtx): void;
|
package/dist/runtimePatcher.js
CHANGED
|
@@ -1,11 +1,130 @@
|
|
|
1
|
-
import { applyShapeGeometry,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { applyShapeGeometry, effectivePos } from './shapes';
|
|
2
|
+
import { computeEdgePath, computeEdgeEndpoints } from './edgePaths';
|
|
3
|
+
import { resolveEdgeLabelPosition, collectEdgeLabels } from './edgeLabels';
|
|
4
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
5
|
+
/** Sanitise a CSS color for use as a marker ID suffix. */
|
|
6
|
+
function colorToMarkerSuffix(color) {
|
|
7
|
+
return color.replace(/[^a-zA-Z0-9]/g, '_');
|
|
8
|
+
}
|
|
9
|
+
/** Return the marker id to use for a marker type with an optional custom stroke and position. */
|
|
10
|
+
function markerIdFor(markerType, stroke, position = 'end') {
|
|
11
|
+
if (markerType === 'none')
|
|
12
|
+
return '';
|
|
13
|
+
const base = `viz-${markerType}`;
|
|
14
|
+
const suffix = position === 'start' ? '-start' : '';
|
|
15
|
+
return stroke
|
|
16
|
+
? `${base}${suffix}-${colorToMarkerSuffix(stroke)}`
|
|
17
|
+
: `${base}${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create the SVG content element(s) for a marker type.
|
|
21
|
+
*/
|
|
22
|
+
function createMarkerContent(markerType, color) {
|
|
23
|
+
switch (markerType) {
|
|
24
|
+
case 'arrow': {
|
|
25
|
+
const p = document.createElementNS(svgNS, 'polygon');
|
|
26
|
+
p.setAttribute('points', '0,2 10,5 0,8');
|
|
27
|
+
p.setAttribute('fill', color);
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
case 'arrowOpen': {
|
|
31
|
+
const p = document.createElementNS(svgNS, 'polyline');
|
|
32
|
+
p.setAttribute('points', '0,2 10,5 0,8');
|
|
33
|
+
p.setAttribute('fill', 'white');
|
|
34
|
+
p.setAttribute('stroke', color);
|
|
35
|
+
p.setAttribute('stroke-width', '1.5');
|
|
36
|
+
p.setAttribute('stroke-linejoin', 'miter');
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
case 'diamond': {
|
|
40
|
+
const p = document.createElementNS(svgNS, 'polygon');
|
|
41
|
+
p.setAttribute('points', '0,5 5,2 10,5 5,8');
|
|
42
|
+
p.setAttribute('fill', color);
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
case 'diamondOpen': {
|
|
46
|
+
const p = document.createElementNS(svgNS, 'polygon');
|
|
47
|
+
p.setAttribute('points', '0,5 5,2 10,5 5,8');
|
|
48
|
+
p.setAttribute('fill', 'white');
|
|
49
|
+
p.setAttribute('stroke', color);
|
|
50
|
+
p.setAttribute('stroke-width', '1.5');
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
case 'circle': {
|
|
54
|
+
const c = document.createElementNS(svgNS, 'circle');
|
|
55
|
+
c.setAttribute('cx', '5');
|
|
56
|
+
c.setAttribute('cy', '5');
|
|
57
|
+
c.setAttribute('r', '3');
|
|
58
|
+
c.setAttribute('fill', color);
|
|
59
|
+
return c;
|
|
60
|
+
}
|
|
61
|
+
case 'circleOpen': {
|
|
62
|
+
const c = document.createElementNS(svgNS, 'circle');
|
|
63
|
+
c.setAttribute('cx', '5');
|
|
64
|
+
c.setAttribute('cy', '5');
|
|
65
|
+
c.setAttribute('r', '3');
|
|
66
|
+
c.setAttribute('fill', 'white');
|
|
67
|
+
c.setAttribute('stroke', color);
|
|
68
|
+
c.setAttribute('stroke-width', '1.5');
|
|
69
|
+
return c;
|
|
70
|
+
}
|
|
71
|
+
case 'square': {
|
|
72
|
+
const r = document.createElementNS(svgNS, 'rect');
|
|
73
|
+
r.setAttribute('x', '2');
|
|
74
|
+
r.setAttribute('y', '2');
|
|
75
|
+
r.setAttribute('width', '6');
|
|
76
|
+
r.setAttribute('height', '6');
|
|
77
|
+
r.setAttribute('fill', color);
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
80
|
+
case 'bar': {
|
|
81
|
+
const l = document.createElementNS(svgNS, 'line');
|
|
82
|
+
l.setAttribute('x1', '5');
|
|
83
|
+
l.setAttribute('y1', '1');
|
|
84
|
+
l.setAttribute('x2', '5');
|
|
85
|
+
l.setAttribute('y2', '9');
|
|
86
|
+
l.setAttribute('stroke', color);
|
|
87
|
+
l.setAttribute('stroke-width', '2');
|
|
88
|
+
l.setAttribute('stroke-linecap', 'round');
|
|
89
|
+
return l;
|
|
90
|
+
}
|
|
91
|
+
case 'halfArrow': {
|
|
92
|
+
const p = document.createElementNS(svgNS, 'polygon');
|
|
93
|
+
p.setAttribute('points', '0,2 10,5 0,5');
|
|
94
|
+
p.setAttribute('fill', color);
|
|
95
|
+
return p;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Ensure a `<marker>` for the given color and type exists inside `<defs>`.
|
|
103
|
+
* Creates one on the fly when the RuntimePatcher encounters a new stroke color or marker type.
|
|
104
|
+
*/
|
|
105
|
+
function ensureColoredMarker(svg, color, markerType = 'arrow', position = 'end') {
|
|
106
|
+
const mid = markerIdFor(markerType, color, position);
|
|
107
|
+
if (!mid)
|
|
108
|
+
return '';
|
|
109
|
+
if (!svg.querySelector(`#${CSS.escape(mid)}`)) {
|
|
110
|
+
const defs = svg.querySelector('defs');
|
|
111
|
+
if (defs) {
|
|
112
|
+
const m = document.createElementNS(svgNS, 'marker');
|
|
113
|
+
m.setAttribute('id', mid);
|
|
114
|
+
m.setAttribute('viewBox', '0 0 10 10');
|
|
115
|
+
m.setAttribute('markerWidth', '10');
|
|
116
|
+
m.setAttribute('markerHeight', '10');
|
|
117
|
+
m.setAttribute('refX', '9');
|
|
118
|
+
m.setAttribute('refY', '5');
|
|
119
|
+
m.setAttribute('orient', position === 'start' ? 'auto-start-reverse' : 'auto');
|
|
120
|
+
const content = createMarkerContent(markerType, color);
|
|
121
|
+
if (content) {
|
|
122
|
+
m.appendChild(content);
|
|
123
|
+
}
|
|
124
|
+
defs.appendChild(m);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return mid;
|
|
9
128
|
}
|
|
10
129
|
export function createRuntimePatchCtx(svg) {
|
|
11
130
|
const nodeGroupsById = new Map();
|
|
@@ -51,10 +170,9 @@ export function createRuntimePatchCtx(svg) {
|
|
|
51
170
|
group.querySelector('.viz-edge-hit');
|
|
52
171
|
if (hit)
|
|
53
172
|
edgeHitsById.set(id, hit);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
edgeLabelsById.set(id, label);
|
|
173
|
+
const labels = Array.from(group.querySelectorAll('[data-viz-role="edge-label"],.viz-edge-label'));
|
|
174
|
+
if (labels.length > 0)
|
|
175
|
+
edgeLabelsById.set(id, labels);
|
|
58
176
|
}
|
|
59
177
|
}
|
|
60
178
|
return {
|
|
@@ -70,20 +188,61 @@ export function createRuntimePatchCtx(svg) {
|
|
|
70
188
|
}
|
|
71
189
|
export function patchRuntime(scene, ctx) {
|
|
72
190
|
const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
|
|
191
|
+
// Pre-compute parent position deltas for container propagation.
|
|
192
|
+
// When a container node moves via runtime, children should follow.
|
|
193
|
+
const parentDeltas = new Map();
|
|
194
|
+
for (const node of scene.nodes) {
|
|
195
|
+
if (node.container) {
|
|
196
|
+
const dx = (node.runtime?.x ?? node.pos.x) - node.pos.x;
|
|
197
|
+
const dy = (node.runtime?.y ?? node.pos.y) - node.pos.y;
|
|
198
|
+
if (dx !== 0 || dy !== 0) {
|
|
199
|
+
parentDeltas.set(node.id, { dx, dy });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
73
203
|
// Nodes: patch geometry + label position + runtime transforms/opacity.
|
|
74
204
|
for (const node of scene.nodes) {
|
|
75
205
|
const group = ctx.nodeGroupsById.get(node.id);
|
|
76
206
|
const shape = ctx.nodeShapesById.get(node.id);
|
|
77
207
|
if (!group || !shape)
|
|
78
208
|
continue;
|
|
79
|
-
|
|
209
|
+
let { x, y } = effectivePos(node);
|
|
210
|
+
// Apply parent container offset so children follow the container
|
|
211
|
+
if (node.parentId) {
|
|
212
|
+
const delta = parentDeltas.get(node.parentId);
|
|
213
|
+
if (delta) {
|
|
214
|
+
x += delta.dx;
|
|
215
|
+
y += delta.dy;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
80
218
|
// Geometry
|
|
81
219
|
applyShapeGeometry(shape, node.shape, { x, y });
|
|
220
|
+
// Container header line (update position if present)
|
|
221
|
+
if (node.container?.headerHeight &&
|
|
222
|
+
'w' in node.shape &&
|
|
223
|
+
'h' in node.shape) {
|
|
224
|
+
const headerLine = group.querySelector('[data-viz-role="container-header"]');
|
|
225
|
+
if (headerLine) {
|
|
226
|
+
const sw = node.shape.w;
|
|
227
|
+
const sh = node.shape.h;
|
|
228
|
+
const headerY = y - sh / 2 + node.container.headerHeight;
|
|
229
|
+
headerLine.setAttribute('x1', String(x - sw / 2));
|
|
230
|
+
headerLine.setAttribute('y1', String(headerY));
|
|
231
|
+
headerLine.setAttribute('x2', String(x + sw / 2));
|
|
232
|
+
headerLine.setAttribute('y2', String(headerY));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
82
235
|
// Label position
|
|
83
236
|
const label = ctx.nodeLabelsById.get(node.id);
|
|
84
237
|
if (label && node.label) {
|
|
85
|
-
|
|
86
|
-
|
|
238
|
+
let lx = x + (node.label.dx || 0);
|
|
239
|
+
let ly = y + (node.label.dy || 0);
|
|
240
|
+
// Container header label centering
|
|
241
|
+
if (node.container?.headerHeight && 'h' in node.shape && !node.label.dy) {
|
|
242
|
+
const sh = node.shape.h;
|
|
243
|
+
ly = y - sh / 2 + node.container.headerHeight / 2;
|
|
244
|
+
lx = x + (node.label.dx || 0);
|
|
245
|
+
}
|
|
87
246
|
label.setAttribute('x', String(lx));
|
|
88
247
|
label.setAttribute('y', String(ly));
|
|
89
248
|
}
|
|
@@ -112,6 +271,18 @@ export function patchRuntime(scene, ctx) {
|
|
|
112
271
|
else {
|
|
113
272
|
group.removeAttribute('transform');
|
|
114
273
|
}
|
|
274
|
+
// Port positions follow the node
|
|
275
|
+
if (node.ports) {
|
|
276
|
+
const portEls = group.querySelectorAll('[data-viz-role="port"]');
|
|
277
|
+
portEls.forEach((portEl) => {
|
|
278
|
+
const portId = portEl.getAttribute('data-port');
|
|
279
|
+
const port = node.ports.find((p) => p.id === portId);
|
|
280
|
+
if (port) {
|
|
281
|
+
portEl.setAttribute('cx', String(x + port.offset.x));
|
|
282
|
+
portEl.setAttribute('cy', String(y + port.offset.y));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
115
286
|
}
|
|
116
287
|
// Edges: patch endpoints + runtime props (opacity, strokeDashoffset) + label + hit.
|
|
117
288
|
for (const edge of scene.edges) {
|
|
@@ -124,24 +295,53 @@ export function patchRuntime(scene, ctx) {
|
|
|
124
295
|
if (!start || !end)
|
|
125
296
|
continue;
|
|
126
297
|
const endpoints = computeEdgeEndpoints(start, end, edge);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
line.setAttribute('
|
|
130
|
-
|
|
131
|
-
|
|
298
|
+
const edgePath = computeEdgePath(endpoints.start, endpoints.end, edge.routing, edge.waypoints);
|
|
299
|
+
// Path
|
|
300
|
+
line.setAttribute('d', edgePath.d);
|
|
301
|
+
// Per-edge style overrides (inline style wins over CSS class defaults)
|
|
302
|
+
if (edge.style?.stroke !== undefined) {
|
|
303
|
+
line.style.stroke = edge.style.stroke;
|
|
304
|
+
}
|
|
305
|
+
if (edge.style?.strokeWidth !== undefined)
|
|
306
|
+
line.style.strokeWidth = String(edge.style.strokeWidth);
|
|
307
|
+
if (edge.style?.fill !== undefined)
|
|
308
|
+
line.style.fill = edge.style.fill;
|
|
309
|
+
if (edge.style?.opacity !== undefined)
|
|
310
|
+
line.style.opacity = String(edge.style.opacity);
|
|
311
|
+
// Update marker-end and marker-start to match edge stroke color
|
|
312
|
+
if (edge.markerEnd && edge.markerEnd !== 'none') {
|
|
313
|
+
const mid = edge.style?.stroke
|
|
314
|
+
? ensureColoredMarker(ctx.svg, edge.style.stroke, edge.markerEnd, 'end')
|
|
315
|
+
: markerIdFor(edge.markerEnd, undefined, 'end');
|
|
316
|
+
line.setAttribute('marker-end', `url(#${mid})`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
line.removeAttribute('marker-end');
|
|
320
|
+
}
|
|
321
|
+
if (edge.markerStart && edge.markerStart !== 'none') {
|
|
322
|
+
const mid = edge.style?.stroke
|
|
323
|
+
? ensureColoredMarker(ctx.svg, edge.style.stroke, edge.markerStart, 'start')
|
|
324
|
+
: markerIdFor(edge.markerStart, undefined, 'start');
|
|
325
|
+
line.setAttribute('marker-start', `url(#${mid})`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
line.removeAttribute('marker-start');
|
|
329
|
+
}
|
|
132
330
|
const hit = ctx.edgeHitsById.get(edge.id);
|
|
133
331
|
if (hit) {
|
|
134
|
-
hit.setAttribute('
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
332
|
+
hit.setAttribute('d', edgePath.d);
|
|
333
|
+
}
|
|
334
|
+
const labelEls = ctx.edgeLabelsById.get(edge.id);
|
|
335
|
+
if (labelEls) {
|
|
336
|
+
const allLabels = collectEdgeLabels(edge);
|
|
337
|
+
labelEls.forEach((el, idx) => {
|
|
338
|
+
const lbl = allLabels[idx];
|
|
339
|
+
if (!lbl)
|
|
340
|
+
return;
|
|
341
|
+
const pos = resolveEdgeLabelPosition(lbl, edgePath);
|
|
342
|
+
el.setAttribute('x', String(pos.x));
|
|
343
|
+
el.setAttribute('y', String(pos.y));
|
|
344
|
+
});
|
|
145
345
|
}
|
|
146
346
|
// Runtime overrides
|
|
147
347
|
if (edge.runtime?.opacity !== undefined) {
|