vizcraft 1.3.0 → 1.4.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 +15 -0
- package/README.md +13 -0
- package/dist/builder.d.ts +24 -1
- package/dist/builder.js +103 -31
- package/dist/edgePaths.d.ts +3 -1
- package/dist/edgePaths.js +42 -12
- package/dist/hitTest.js +33 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtimePatcher.js +19 -21
- package/dist/shapes.d.ts +6 -0
- package/dist/shapes.js +122 -0
- package/dist/types.d.ts +24 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# vizcraft
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#86](https://github.com/ChipiKaf/vizcraft/pull/86) [`04df81d`](https://github.com/ChipiKaf/vizcraft/commit/04df81dfa0e2f6f4a54232f28382d285de3030c6) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Support dangling edges with free endpoints (source-only or target-only) for interactive diagrams. Added `danglingEdge()` builder method, `fromAt()`/`toAt()` on `EdgeBuilder`, and made `VizEdge.from`/`VizEdge.to` optional. Dangling edges work with all edge features including routing, markers, labels, styling, hit testing, SVG export, and DOM mounting.
|
|
8
|
+
|
|
9
|
+
- [#88](https://github.com/ChipiKaf/vizcraft/pull/88) [`28390fc`](https://github.com/ChipiKaf/vizcraft/commit/28390fc49d614dbb5c5f3c6a576b52a527a2b4dc) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - - Add freeform perimeter anchors for edges (`fromAngle` / `toAngle`). Edges can now leave or arrive at a fixed angle on any node shape, overriding the default boundary projection. Supported via fluent `.fromAngle(deg)` / `.toAngle(deg)` methods and declarative `EdgeOptions`. Also exports `computeNodeAnchorAtAngle(node, angleDeg)` for advanced use.
|
|
10
|
+
- Support dangling edges with free endpoints (source-only or target-only) for interactive diagrams. Added `danglingEdge()` builder method, `fromAt()`/`toAt()` on `EdgeBuilder`, and made `VizEdge.from`/`VizEdge.to` optional. Dangling edges work with all edge features including routing, markers, labels, styling, hit testing, SVG export, and DOM mounting.
|
|
11
|
+
|
|
12
|
+
## 1.3.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [`086ef9e`](https://github.com/ChipiKaf/vizcraft/commit/086ef9e7b5d505cbb03f955e8d24297fb60a6b3e) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - The fix prevents commit() from using a stale cached runtimePatchCtx (which could reference detached DOM elements) by always recreating it after \_renderSceneToDOM, and removes the redundant strokeDasharray write from patchRuntime so that base style is owned by a single write path.
|
|
17
|
+
|
|
3
18
|
## 1.3.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ VizCraft is designed to make creating beautiful, animated node-link diagrams and
|
|
|
20
20
|
- **Two Animation Systems**: Lightweight registry/CSS animations (e.g. edge `flow`) and data-only timeline animations (`AnimationSpec`).
|
|
21
21
|
- **Framework Agnostic**: The core logic is pure TypeScript and can be used with any framework or Vanilla JS.
|
|
22
22
|
- **Custom Overlays**: Create complex, custom UI elements that float on top of your visualization.
|
|
23
|
+
- **Dangling Edges**: Create edges with free endpoints for drag-to-connect interactions.
|
|
23
24
|
|
|
24
25
|
## 📦 Installation
|
|
25
26
|
|
|
@@ -307,6 +308,12 @@ b.edge('srv', 'db').fromPort('out-1').toPort('in').arrow();
|
|
|
307
308
|
|
|
308
309
|
// Default ports (no .port() needed) — every shape has built-in ports
|
|
309
310
|
b.edge('a', 'b').fromPort('right').toPort('left').arrow();
|
|
311
|
+
|
|
312
|
+
// Dangling edges — one or both endpoints at a free coordinate
|
|
313
|
+
b.danglingEdge('preview').from('srv').toAt({ x: 300, y: 200 }).arrow().dashed();
|
|
314
|
+
|
|
315
|
+
// Declarative dangling edge
|
|
316
|
+
b.danglingEdge('e1', { from: 'srv', toAt: { x: 300, y: 200 }, arrow: true });
|
|
310
317
|
```
|
|
311
318
|
|
|
312
319
|
| Method | Description |
|
|
@@ -323,6 +330,12 @@ b.edge('a', 'b').fromPort('right').toPort('left').arrow();
|
|
|
323
330
|
| `.markerStart(type)` | Set marker type at the source end. See `EdgeMarkerType`. |
|
|
324
331
|
| `.fromPort(portId)` | Connect from a specific named port on the source node. |
|
|
325
332
|
| `.toPort(portId)` | Connect to a specific named port on the target node. |
|
|
333
|
+
| `.fromAngle(deg)` | Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the source node. |
|
|
334
|
+
| `.toAngle(deg)` | Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the target node. |
|
|
335
|
+
| `.from(nodeId)` | Attach the source end to an existing node (useful with `danglingEdge()`). |
|
|
336
|
+
| `.to(nodeId)` | Attach the target end to an existing node (useful with `danglingEdge()`). |
|
|
337
|
+
| `.fromAt(pos)` | Set the free-endpoint coordinate for the source end (`{ x, y }`). |
|
|
338
|
+
| `.toAt(pos)` | Set the free-endpoint coordinate for the target end (`{ x, y }`). |
|
|
326
339
|
| `.stroke(color, width?)` | Set stroke color and optional width. |
|
|
327
340
|
| `.fill(color)` | Set fill color. |
|
|
328
341
|
| `.opacity(value)` | Set opacity (0–1). |
|
package/dist/builder.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, RichText, RichTextToken, AnimationConfig, OverlayId, OverlayParams, VizGridConfig, ContainerConfig, EdgeRouting, EdgeMarkerType, EdgePathResolver, NodeOptions, EdgeOptions, PanZoomOptions, PanZoomController, VizSceneMutator, VizPlugin, VizEventMap, LayoutAlgorithm, SvgExportOptions } from './types';
|
|
1
|
+
import type { Vec2, VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, RichText, RichTextToken, AnimationConfig, OverlayId, OverlayParams, VizGridConfig, ContainerConfig, EdgeRouting, EdgeMarkerType, EdgePathResolver, NodeOptions, EdgeOptions, PanZoomOptions, PanZoomController, VizSceneMutator, VizPlugin, VizEventMap, LayoutAlgorithm, SvgExportOptions } from './types';
|
|
2
2
|
import { OverlayBuilder } from './overlayBuilder';
|
|
3
3
|
import type { AnimationSpec } from './anim/spec';
|
|
4
4
|
import { type AnimationBuilder, type AnimatableProps, type TweenOptions } from './anim/animationBuilder';
|
|
@@ -56,6 +56,13 @@ export interface VizBuilder extends VizSceneMutator {
|
|
|
56
56
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
57
57
|
/** Create a fully-configured edge declaratively and return the parent VizBuilder. */
|
|
58
58
|
edge(from: string, to: string, opts: EdgeOptions): VizBuilder;
|
|
59
|
+
/**
|
|
60
|
+
* Create a dangling edge with at least one free endpoint.
|
|
61
|
+
* The free end renders at a canvas coordinate (`fromAt`/`toAt`) rather than a node.
|
|
62
|
+
*/
|
|
63
|
+
danglingEdge(id: string, opts?: EdgeOptions): EdgeBuilder;
|
|
64
|
+
/** Declarative overload — returns the parent VizBuilder. */
|
|
65
|
+
danglingEdge(id: string, opts: EdgeOptions): VizBuilder;
|
|
59
66
|
/** Hydrates the builder from an existing VizScene. */
|
|
60
67
|
fromScene(scene: VizScene): VizBuilder;
|
|
61
68
|
build(): VizScene;
|
|
@@ -257,6 +264,8 @@ interface NodeBuilder {
|
|
|
257
264
|
node(id: string, opts: NodeOptions): VizBuilder;
|
|
258
265
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
259
266
|
edge(from: string, to: string, opts: EdgeOptions): VizBuilder;
|
|
267
|
+
danglingEdge(id: string, opts?: EdgeOptions): EdgeBuilder;
|
|
268
|
+
danglingEdge(id: string, opts: EdgeOptions): VizBuilder;
|
|
260
269
|
overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
|
|
261
270
|
overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
|
|
262
271
|
overlay<T>(id: string, params: T, key?: string): VizBuilder;
|
|
@@ -269,6 +278,14 @@ interface EdgeBuilder {
|
|
|
269
278
|
orthogonal(): EdgeBuilder;
|
|
270
279
|
routing(mode: EdgeRouting): EdgeBuilder;
|
|
271
280
|
via(x: number, y: number): EdgeBuilder;
|
|
281
|
+
/** Set the source node id (useful with `danglingEdge()`). */
|
|
282
|
+
from(nodeId: string): EdgeBuilder;
|
|
283
|
+
/** Set the target node id (useful with `danglingEdge()`). */
|
|
284
|
+
to(nodeId: string): EdgeBuilder;
|
|
285
|
+
/** Set the free-endpoint source position (when `from` is omitted). */
|
|
286
|
+
fromAt(pos: Vec2): EdgeBuilder;
|
|
287
|
+
/** Set the free-endpoint target position (when `to` is omitted). */
|
|
288
|
+
toAt(pos: Vec2): EdgeBuilder;
|
|
272
289
|
label(text: string, opts?: Partial<EdgeLabel>): EdgeBuilder;
|
|
273
290
|
/**
|
|
274
291
|
* Create a rich text label (mixed formatting) using nested SVG <tspan> spans.
|
|
@@ -293,6 +310,10 @@ interface EdgeBuilder {
|
|
|
293
310
|
fromPort(portId: string): EdgeBuilder;
|
|
294
311
|
/** Connect the edge to a specific port on the target node. */
|
|
295
312
|
toPort(portId: string): EdgeBuilder;
|
|
313
|
+
/** Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the source node. */
|
|
314
|
+
fromAngle(deg: number): EdgeBuilder;
|
|
315
|
+
/** Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the target node. */
|
|
316
|
+
toAngle(deg: number): EdgeBuilder;
|
|
296
317
|
connect(anchor: 'center' | 'boundary'): EdgeBuilder;
|
|
297
318
|
/** Sets the fill color of the edge path. */
|
|
298
319
|
fill(color: string): EdgeBuilder;
|
|
@@ -333,6 +354,8 @@ interface EdgeBuilder {
|
|
|
333
354
|
node(id: string, opts: NodeOptions): VizBuilder;
|
|
334
355
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
335
356
|
edge(from: string, to: string, opts: EdgeOptions): VizBuilder;
|
|
357
|
+
danglingEdge(id: string, opts?: EdgeOptions): EdgeBuilder;
|
|
358
|
+
danglingEdge(id: string, opts: EdgeOptions): VizBuilder;
|
|
336
359
|
overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
|
|
337
360
|
overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
|
|
338
361
|
overlay<T>(id: string, params: T, key?: string): VizBuilder;
|
package/dist/builder.js
CHANGED
|
@@ -43,6 +43,21 @@ const SHADOW_DEFAULTS = {
|
|
|
43
43
|
blur: 4,
|
|
44
44
|
color: 'rgba(0,0,0,0.2)',
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Resolves edge endpoints for dangling edges.
|
|
48
|
+
* Returns `null` when a referenced node id doesn't exist.
|
|
49
|
+
*/
|
|
50
|
+
function resolveDanglingEdge(edge, nodesById) {
|
|
51
|
+
const start = edge.from ? (nodesById.get(edge.from) ?? null) : null;
|
|
52
|
+
const end = edge.to ? (nodesById.get(edge.to) ?? null) : null;
|
|
53
|
+
if (edge.from && !start)
|
|
54
|
+
return null;
|
|
55
|
+
if (edge.to && !end)
|
|
56
|
+
return null;
|
|
57
|
+
if (!start && !edge.fromAt && !end && !edge.toAt)
|
|
58
|
+
return null;
|
|
59
|
+
return { start, end };
|
|
60
|
+
}
|
|
46
61
|
/** Apply defaults to a partial shadow config. */
|
|
47
62
|
function resolveShadow(shadow) {
|
|
48
63
|
return {
|
|
@@ -382,8 +397,16 @@ function applyNodeOptions(nb, opts) {
|
|
|
382
397
|
if (opts.parent)
|
|
383
398
|
nb.parent(opts.parent);
|
|
384
399
|
}
|
|
385
|
-
/**
|
|
400
|
+
/** Applies an `EdgeOptions` object to an `EdgeBuilder`. */
|
|
386
401
|
function applyEdgeOptions(eb, opts) {
|
|
402
|
+
if (opts.from)
|
|
403
|
+
eb.from(opts.from);
|
|
404
|
+
if (opts.to)
|
|
405
|
+
eb.to(opts.to);
|
|
406
|
+
if (opts.fromAt)
|
|
407
|
+
eb.fromAt(opts.fromAt);
|
|
408
|
+
if (opts.toAt)
|
|
409
|
+
eb.toAt(opts.toAt);
|
|
387
410
|
// Routing
|
|
388
411
|
if (opts.routing)
|
|
389
412
|
eb.routing(opts.routing);
|
|
@@ -426,6 +449,10 @@ function applyEdgeOptions(eb, opts) {
|
|
|
426
449
|
eb.fromPort(opts.fromPort);
|
|
427
450
|
if (opts.toPort)
|
|
428
451
|
eb.toPort(opts.toPort);
|
|
452
|
+
if (opts.fromAngle !== undefined)
|
|
453
|
+
eb.fromAngle(opts.fromAngle);
|
|
454
|
+
if (opts.toAngle !== undefined)
|
|
455
|
+
eb.toAngle(opts.toAngle);
|
|
429
456
|
// Labels
|
|
430
457
|
if (opts.label) {
|
|
431
458
|
if (typeof opts.label === 'string') {
|
|
@@ -654,14 +681,13 @@ class VizBuilderImpl {
|
|
|
654
681
|
// The reconciliation correctly re-uses existing SVG elements, inserts new ones, and deletes missing ones.
|
|
655
682
|
const scene = this.build();
|
|
656
683
|
this._renderSceneToDOM(scene, container);
|
|
657
|
-
// Apply runtime overrides (if any)
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
684
|
+
// Apply runtime overrides (if any).
|
|
685
|
+
// Always recreate the context after _renderSceneToDOM so patchRuntime
|
|
686
|
+
// never references stale / detached elements (fixes #81).
|
|
687
|
+
const ctx = createRuntimePatchCtx(svg, {
|
|
688
|
+
edgePathResolver: this._edgePathResolver,
|
|
689
|
+
});
|
|
690
|
+
runtimePatchCtxBySvg.set(svg, ctx);
|
|
665
691
|
patchRuntime(scene, ctx);
|
|
666
692
|
}
|
|
667
693
|
/**
|
|
@@ -787,6 +813,22 @@ class VizBuilderImpl {
|
|
|
787
813
|
applyEdgeOptions(eb, idOrOpts);
|
|
788
814
|
return this;
|
|
789
815
|
}
|
|
816
|
+
danglingEdge(id, opts) {
|
|
817
|
+
if (!this._edges.has(id)) {
|
|
818
|
+
const edgeDef = { id };
|
|
819
|
+
if (opts?.fromAt)
|
|
820
|
+
edgeDef.fromAt = opts.fromAt;
|
|
821
|
+
if (opts?.toAt)
|
|
822
|
+
edgeDef.toAt = opts.toAt;
|
|
823
|
+
this._edges.set(id, edgeDef);
|
|
824
|
+
this._edgeOrder.push(id);
|
|
825
|
+
}
|
|
826
|
+
const eb = new EdgeBuilderImpl(this, this._edges.get(id));
|
|
827
|
+
if (!opts)
|
|
828
|
+
return eb;
|
|
829
|
+
applyEdgeOptions(eb, opts);
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
790
832
|
/**
|
|
791
833
|
* Hydrates the builder from an existing VizScene.
|
|
792
834
|
* @param scene The scene to hydrate from
|
|
@@ -834,10 +876,12 @@ class VizBuilderImpl {
|
|
|
834
876
|
*/
|
|
835
877
|
build() {
|
|
836
878
|
this._edges.forEach((edge) => {
|
|
837
|
-
|
|
879
|
+
// Only warn about missing nodes when a node id is specified
|
|
880
|
+
// (dangling edges intentionally omit from/to).
|
|
881
|
+
if (edge.from && !this._nodes.has(edge.from)) {
|
|
838
882
|
console.warn(`VizBuilder: Edge ${edge.id} references missing source node ${edge.from}`);
|
|
839
883
|
}
|
|
840
|
-
if (!this._nodes.has(edge.to)) {
|
|
884
|
+
if (edge.to && !this._nodes.has(edge.to)) {
|
|
841
885
|
console.warn(`VizBuilder: Edge ${edge.id} references missing target node ${edge.to}`);
|
|
842
886
|
}
|
|
843
887
|
});
|
|
@@ -1272,10 +1316,10 @@ class VizBuilderImpl {
|
|
|
1272
1316
|
});
|
|
1273
1317
|
const processedEdgeIds = new Set();
|
|
1274
1318
|
edges.forEach((edge) => {
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
if (!start || !end)
|
|
1319
|
+
const resolved = resolveDanglingEdge(edge, nodesById);
|
|
1320
|
+
if (!resolved)
|
|
1278
1321
|
return;
|
|
1322
|
+
const { start, end } = resolved;
|
|
1279
1323
|
processedEdgeIds.add(edge.id);
|
|
1280
1324
|
let group = existingEdgesMap.get(edge.id);
|
|
1281
1325
|
if (!group) {
|
|
@@ -1323,7 +1367,7 @@ class VizBuilderImpl {
|
|
|
1323
1367
|
group.setAttribute('class', classes);
|
|
1324
1368
|
// Use effective positions (handles runtime overrides internally via helper)
|
|
1325
1369
|
let edgePath;
|
|
1326
|
-
if (start === end) {
|
|
1370
|
+
if (start && end && start === end) {
|
|
1327
1371
|
edgePath = computeSelfLoop(start, edge);
|
|
1328
1372
|
}
|
|
1329
1373
|
else {
|
|
@@ -1333,13 +1377,12 @@ class VizBuilderImpl {
|
|
|
1333
1377
|
// Allow consumer override of the SVG path `d` string.
|
|
1334
1378
|
if (this._edgePathResolver) {
|
|
1335
1379
|
const defaultResolver = (e) => {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
if (!s || !t)
|
|
1380
|
+
const r = resolveDanglingEdge(e, nodesById);
|
|
1381
|
+
if (!r)
|
|
1339
1382
|
return '';
|
|
1340
|
-
if (
|
|
1341
|
-
return computeSelfLoop(
|
|
1342
|
-
const endpoints = computeEdgeEndpoints(
|
|
1383
|
+
if (r.start && r.end && r.start === r.end)
|
|
1384
|
+
return computeSelfLoop(r.start, e).d;
|
|
1385
|
+
const endpoints = computeEdgeEndpoints(r.start, r.end, e);
|
|
1343
1386
|
return computeEdgePath(endpoints.start, endpoints.end, e.routing, e.waypoints).d;
|
|
1344
1387
|
};
|
|
1345
1388
|
try {
|
|
@@ -1946,10 +1989,10 @@ class VizBuilderImpl {
|
|
|
1946
1989
|
// Render Edges
|
|
1947
1990
|
svgContent += '<g class="viz-layer-edges" data-viz-layer="edges">';
|
|
1948
1991
|
exportEdges.forEach((edge) => {
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
if (!start || !end)
|
|
1992
|
+
const resolved = resolveDanglingEdge(edge, nodesById);
|
|
1993
|
+
if (!resolved)
|
|
1952
1994
|
return;
|
|
1995
|
+
const { start, end } = resolved;
|
|
1953
1996
|
// Animations
|
|
1954
1997
|
let animClasses = '';
|
|
1955
1998
|
let animStyleStr = '';
|
|
@@ -1984,7 +2027,7 @@ class VizBuilderImpl {
|
|
|
1984
2027
|
? `marker-start="url(#${markerIdFor(edge.markerStart, edge.style?.stroke, 'start')})"`
|
|
1985
2028
|
: '';
|
|
1986
2029
|
let edgePath;
|
|
1987
|
-
if (start === end) {
|
|
2030
|
+
if (start && end && start === end) {
|
|
1988
2031
|
edgePath = computeSelfLoop(start, edge);
|
|
1989
2032
|
}
|
|
1990
2033
|
else {
|
|
@@ -1993,13 +2036,12 @@ class VizBuilderImpl {
|
|
|
1993
2036
|
}
|
|
1994
2037
|
if (this._edgePathResolver) {
|
|
1995
2038
|
const defaultResolver = (e) => {
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
if (!s || !t)
|
|
2039
|
+
const r = resolveDanglingEdge(e, nodesById);
|
|
2040
|
+
if (!r)
|
|
1999
2041
|
return '';
|
|
2000
|
-
if (
|
|
2001
|
-
return computeSelfLoop(
|
|
2002
|
-
const endpoints = computeEdgeEndpoints(
|
|
2042
|
+
if (r.start && r.end && r.start === r.end)
|
|
2043
|
+
return computeSelfLoop(r.start, e).d;
|
|
2044
|
+
const endpoints = computeEdgeEndpoints(r.start, r.end, e);
|
|
2003
2045
|
return computeEdgePath(endpoints.start, endpoints.end, e.routing, e.waypoints).d;
|
|
2004
2046
|
};
|
|
2005
2047
|
try {
|
|
@@ -2626,6 +2668,9 @@ class NodeBuilderImpl {
|
|
|
2626
2668
|
edge(from, to, idOrOpts) {
|
|
2627
2669
|
return this._builder.edge(from, to, idOrOpts);
|
|
2628
2670
|
}
|
|
2671
|
+
danglingEdge(id, opts) {
|
|
2672
|
+
return this._builder.danglingEdge(id, opts);
|
|
2673
|
+
}
|
|
2629
2674
|
overlay(arg1, arg2, arg3) {
|
|
2630
2675
|
if (typeof arg1 === 'function')
|
|
2631
2676
|
return this._builder.overlay(arg1);
|
|
@@ -2668,6 +2713,22 @@ class EdgeBuilderImpl {
|
|
|
2668
2713
|
this.edgeDef.waypoints.push({ x, y });
|
|
2669
2714
|
return this;
|
|
2670
2715
|
}
|
|
2716
|
+
from(nodeId) {
|
|
2717
|
+
this.edgeDef.from = nodeId;
|
|
2718
|
+
return this;
|
|
2719
|
+
}
|
|
2720
|
+
to(nodeId) {
|
|
2721
|
+
this.edgeDef.to = nodeId;
|
|
2722
|
+
return this;
|
|
2723
|
+
}
|
|
2724
|
+
fromAt(pos) {
|
|
2725
|
+
this.edgeDef.fromAt = pos;
|
|
2726
|
+
return this;
|
|
2727
|
+
}
|
|
2728
|
+
toAt(pos) {
|
|
2729
|
+
this.edgeDef.toAt = pos;
|
|
2730
|
+
return this;
|
|
2731
|
+
}
|
|
2671
2732
|
label(text, opts) {
|
|
2672
2733
|
const lbl = { position: 'mid', text, dy: -10, ...opts };
|
|
2673
2734
|
// Accumulate into the labels array
|
|
@@ -2739,6 +2800,14 @@ class EdgeBuilderImpl {
|
|
|
2739
2800
|
this.edgeDef.toPort = portId;
|
|
2740
2801
|
return this;
|
|
2741
2802
|
}
|
|
2803
|
+
fromAngle(deg) {
|
|
2804
|
+
this.edgeDef.fromAngle = deg;
|
|
2805
|
+
return this;
|
|
2806
|
+
}
|
|
2807
|
+
toAngle(deg) {
|
|
2808
|
+
this.edgeDef.toAngle = deg;
|
|
2809
|
+
return this;
|
|
2810
|
+
}
|
|
2742
2811
|
fill(color) {
|
|
2743
2812
|
this.edgeDef.style = {
|
|
2744
2813
|
...(this.edgeDef.style || {}),
|
|
@@ -2855,6 +2924,9 @@ class EdgeBuilderImpl {
|
|
|
2855
2924
|
edge(from, to, idOrOpts) {
|
|
2856
2925
|
return this.parent.edge(from, to, idOrOpts);
|
|
2857
2926
|
}
|
|
2927
|
+
danglingEdge(id, opts) {
|
|
2928
|
+
return this.parent.danglingEdge(id, opts);
|
|
2929
|
+
}
|
|
2858
2930
|
overlay(arg1, arg2, arg3) {
|
|
2859
2931
|
if (typeof arg1 === 'function')
|
|
2860
2932
|
return this.parent.overlay(arg1);
|
package/dist/edgePaths.d.ts
CHANGED
|
@@ -24,11 +24,13 @@ export interface EdgePathResult {
|
|
|
24
24
|
* to the port's absolute position. Otherwise the legacy `anchor` mode
|
|
25
25
|
* (`'center'` | `'boundary'`) is used.
|
|
26
26
|
*
|
|
27
|
+
* Accepts `null` for either node to support dangling edges.
|
|
28
|
+
*
|
|
27
29
|
* This replicates the logic used internally by the core builder and
|
|
28
30
|
* runtime patcher so that external renderers (e.g. React) can
|
|
29
31
|
* resolve boundary anchors consistently.
|
|
30
32
|
*/
|
|
31
|
-
export declare function computeEdgeEndpoints(start: VizNode, end: VizNode, edge: VizEdge): {
|
|
33
|
+
export declare function computeEdgeEndpoints(start: VizNode | null, end: VizNode | null, edge: VizEdge): {
|
|
32
34
|
start: Vec2;
|
|
33
35
|
end: Vec2;
|
|
34
36
|
};
|
package/dist/edgePaths.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 1. An SVG `d` attribute string (for `<path>` elements).
|
|
7
7
|
* 2. A midpoint along the path (for label positioning).
|
|
8
8
|
*/
|
|
9
|
-
import { computeNodeAnchor, effectivePos, effectiveShape, resolvePortPosition, } from './shapes';
|
|
9
|
+
import { computeNodeAnchor, computeNodeAnchorAtAngle, effectivePos, effectiveShape, resolvePortPosition, } from './shapes';
|
|
10
10
|
/**
|
|
11
11
|
* Compute anchor-resolved start/end points for an edge.
|
|
12
12
|
*
|
|
@@ -14,23 +14,53 @@ import { computeNodeAnchor, effectivePos, effectiveShape, resolvePortPosition, }
|
|
|
14
14
|
* to the port's absolute position. Otherwise the legacy `anchor` mode
|
|
15
15
|
* (`'center'` | `'boundary'`) is used.
|
|
16
16
|
*
|
|
17
|
+
* Accepts `null` for either node to support dangling edges.
|
|
18
|
+
*
|
|
17
19
|
* This replicates the logic used internally by the core builder and
|
|
18
20
|
* runtime patcher so that external renderers (e.g. React) can
|
|
19
21
|
* resolve boundary anchors consistently.
|
|
20
22
|
*/
|
|
21
23
|
export function computeEdgeEndpoints(start, end, edge) {
|
|
22
24
|
const anchor = edge.anchor ?? 'boundary';
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const freeStart = edge.fromAt;
|
|
26
|
+
const freeEnd = edge.toAt;
|
|
27
|
+
// Determine the "other" position for anchor resolution.
|
|
28
|
+
const endTarget = end ? effectivePos(end) : (freeEnd ?? { x: 0, y: 0 });
|
|
29
|
+
const startTarget = start
|
|
30
|
+
? effectivePos(start)
|
|
31
|
+
: (freeStart ?? { x: 0, y: 0 });
|
|
32
|
+
// Source endpoint
|
|
33
|
+
let startAnchor;
|
|
34
|
+
if (start) {
|
|
35
|
+
if (edge.fromAngle !== undefined) {
|
|
36
|
+
startAnchor = computeNodeAnchorAtAngle(start, edge.fromAngle);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
startAnchor = edge.fromPort
|
|
40
|
+
? (resolvePortPosition(start, edge.fromPort) ??
|
|
41
|
+
computeNodeAnchor(start, endTarget, anchor))
|
|
42
|
+
: computeNodeAnchor(start, endTarget, anchor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
startAnchor = freeStart ?? { x: 0, y: 0 };
|
|
47
|
+
}
|
|
48
|
+
// Target endpoint
|
|
49
|
+
let endAnchor;
|
|
50
|
+
if (end) {
|
|
51
|
+
if (edge.toAngle !== undefined) {
|
|
52
|
+
endAnchor = computeNodeAnchorAtAngle(end, edge.toAngle);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
endAnchor = edge.toPort
|
|
56
|
+
? (resolvePortPosition(end, edge.toPort) ??
|
|
57
|
+
computeNodeAnchor(end, startTarget, anchor))
|
|
58
|
+
: computeNodeAnchor(end, startTarget, anchor);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
endAnchor = freeEnd ?? { x: 0, y: 0 };
|
|
63
|
+
}
|
|
34
64
|
return { start: startAnchor, end: endAnchor };
|
|
35
65
|
}
|
|
36
66
|
/**
|
package/dist/hitTest.js
CHANGED
|
@@ -264,19 +264,31 @@ export function hitTestRect(scene, rect) {
|
|
|
264
264
|
// Check edges
|
|
265
265
|
// Roughly test if the edge's bounding box intersects the rect
|
|
266
266
|
for (const edge of scene.edges) {
|
|
267
|
-
const startNode =
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
const startNode = edge.from
|
|
268
|
+
? scene.nodes.find((n) => n.id === edge.from)
|
|
269
|
+
: undefined;
|
|
270
|
+
const endNode = edge.to
|
|
271
|
+
? scene.nodes.find((n) => n.id === edge.to)
|
|
272
|
+
: undefined;
|
|
273
|
+
if (edge.from && !startNode)
|
|
274
|
+
continue;
|
|
275
|
+
if (edge.to && !endNode)
|
|
276
|
+
continue;
|
|
277
|
+
if (!startNode && !edge.fromAt && !endNode && !edge.toAt)
|
|
270
278
|
continue;
|
|
271
279
|
// Simplistic edge bounding box
|
|
272
|
-
const startPos =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
const startPos = startNode
|
|
281
|
+
? {
|
|
282
|
+
x: startNode.runtime?.x ?? startNode.pos.x,
|
|
283
|
+
y: startNode.runtime?.y ?? startNode.pos.y,
|
|
284
|
+
}
|
|
285
|
+
: (edge.fromAt ?? { x: 0, y: 0 });
|
|
286
|
+
const endPos = endNode
|
|
287
|
+
? {
|
|
288
|
+
x: endNode.runtime?.x ?? endNode.pos.x,
|
|
289
|
+
y: endNode.runtime?.y ?? endNode.pos.y,
|
|
290
|
+
}
|
|
291
|
+
: (edge.toAt ?? { x: 0, y: 0 });
|
|
280
292
|
const minX = Math.min(startPos.x, endPos.x) - 10;
|
|
281
293
|
const maxX = Math.max(startPos.x, endPos.x) + 10;
|
|
282
294
|
const minY = Math.min(startPos.y, endPos.y) - 10;
|
|
@@ -328,11 +340,17 @@ export function edgeDistance(scene, edgeId, point) {
|
|
|
328
340
|
const edge = scene.edges.find((e) => e.id === edgeId);
|
|
329
341
|
if (!edge)
|
|
330
342
|
return Infinity;
|
|
331
|
-
const startNode =
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
const startNode = edge.from
|
|
344
|
+
? scene.nodes.find((n) => n.id === edge.from)
|
|
345
|
+
: null;
|
|
346
|
+
const endNode = edge.to ? scene.nodes.find((n) => n.id === edge.to) : null;
|
|
347
|
+
if (edge.from && !startNode)
|
|
348
|
+
return Infinity;
|
|
349
|
+
if (edge.to && !endNode)
|
|
350
|
+
return Infinity;
|
|
351
|
+
if (!startNode && !edge.fromAt && !endNode && !edge.toAt)
|
|
334
352
|
return Infinity;
|
|
335
|
-
const { start, end } = computeEdgeEndpoints(startNode, endNode, edge);
|
|
353
|
+
const { start, end } = computeEdgeEndpoints(startNode ?? null, endNode ?? null, edge);
|
|
336
354
|
const pathData = computeEdgePath(start, end, edge.routing, edge.waypoints);
|
|
337
355
|
const polyline = samplePath(pathData.d, 10);
|
|
338
356
|
return distToPolyline(point, polyline);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './overlayBuilder';
|
|
|
8
8
|
export * from './edgePaths';
|
|
9
9
|
export * from './edgeLabels';
|
|
10
10
|
export * from './edgeStyles';
|
|
11
|
-
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, } from './shapes';
|
|
11
|
+
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, computeNodeAnchorAtAngle, } from './shapes';
|
|
12
12
|
export * from './anim/spec';
|
|
13
13
|
export * from './anim/animationBuilder';
|
|
14
14
|
export * from './anim/playback';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export * from './overlayBuilder';
|
|
|
8
8
|
export * from './edgePaths';
|
|
9
9
|
export * from './edgeLabels';
|
|
10
10
|
export * from './edgeStyles';
|
|
11
|
-
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, } from './shapes';
|
|
11
|
+
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, computeNodeAnchorAtAngle, } from './shapes';
|
|
12
12
|
export * from './anim/spec';
|
|
13
13
|
export * from './anim/animationBuilder';
|
|
14
14
|
export * from './anim/playback';
|
package/dist/runtimePatcher.js
CHANGED
|
@@ -511,19 +511,9 @@ export function patchRuntime(scene, ctx) {
|
|
|
511
511
|
shape.removeAttribute('opacity');
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (resolved) {
|
|
518
|
-
shape.setAttribute('stroke-dasharray', resolved);
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
shape.removeAttribute('stroke-dasharray');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
shape.removeAttribute('stroke-dasharray');
|
|
526
|
-
}
|
|
514
|
+
// NOTE: strokeDasharray is a static base style — written exclusively by
|
|
515
|
+
// _renderSceneToDOM via setSvgAttributes. patchRuntime must NOT duplicate
|
|
516
|
+
// that write to avoid the stale-context overwrite described in #81.
|
|
527
517
|
if (node.style?.shadow) {
|
|
528
518
|
const fid = ensureShadowFilter(ctx.svg, node.style.shadow);
|
|
529
519
|
shape.setAttribute('filter', `url(#${fid})`);
|
|
@@ -581,12 +571,16 @@ export function patchRuntime(scene, ctx) {
|
|
|
581
571
|
const line = ctx.edgeLinesById.get(edge.id);
|
|
582
572
|
if (!group || !line)
|
|
583
573
|
continue;
|
|
584
|
-
const start = nodesById.get(edge.from);
|
|
585
|
-
const end = nodesById.get(edge.to);
|
|
586
|
-
if (
|
|
574
|
+
const start = edge.from ? (nodesById.get(edge.from) ?? null) : null;
|
|
575
|
+
const end = edge.to ? (nodesById.get(edge.to) ?? null) : null;
|
|
576
|
+
if (edge.from && !start)
|
|
577
|
+
continue;
|
|
578
|
+
if (edge.to && !end)
|
|
579
|
+
continue;
|
|
580
|
+
if (!start && !edge.fromAt && !end && !edge.toAt)
|
|
587
581
|
continue;
|
|
588
582
|
let edgePath;
|
|
589
|
-
if (start === end) {
|
|
583
|
+
if (start && end && start === end) {
|
|
590
584
|
edgePath = computeSelfLoop(start, edge);
|
|
591
585
|
}
|
|
592
586
|
else {
|
|
@@ -595,11 +589,15 @@ export function patchRuntime(scene, ctx) {
|
|
|
595
589
|
}
|
|
596
590
|
if (edgePathResolver) {
|
|
597
591
|
const defaultResolver = (e) => {
|
|
598
|
-
const s = nodesById.get(e.from);
|
|
599
|
-
const t = nodesById.get(e.to);
|
|
600
|
-
if (
|
|
592
|
+
const s = e.from ? (nodesById.get(e.from) ?? null) : null;
|
|
593
|
+
const t = e.to ? (nodesById.get(e.to) ?? null) : null;
|
|
594
|
+
if (e.from && !s)
|
|
595
|
+
return '';
|
|
596
|
+
if (e.to && !t)
|
|
597
|
+
return '';
|
|
598
|
+
if (!s && !e.fromAt && !t && !e.toAt)
|
|
601
599
|
return '';
|
|
602
|
-
if (s === t)
|
|
600
|
+
if (s && t && s === t)
|
|
603
601
|
return computeSelfLoop(s, e).d;
|
|
604
602
|
const endpoints = computeEdgeEndpoints(s, t, e);
|
|
605
603
|
return computeEdgePath(endpoints.start, endpoints.end, e.routing, e.waypoints).d;
|
package/dist/shapes.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface ShapeBehavior<K extends NodeShape['kind']> {
|
|
|
12
12
|
anchorBoundary(pos: Vec2, target: Vec2, shape: Extract<NodeShape, {
|
|
13
13
|
kind: K;
|
|
14
14
|
}>): Vec2;
|
|
15
|
+
/** Resolve a perimeter point at the given angle (degrees, 0 = right, 90 = down). */
|
|
16
|
+
anchorAtAngle(pos: Vec2, angleDeg: number, shape: Extract<NodeShape, {
|
|
17
|
+
kind: K;
|
|
18
|
+
}>): Vec2;
|
|
15
19
|
}
|
|
16
20
|
export declare function effectivePos(node: VizNode): Vec2;
|
|
17
21
|
export declare function effectiveShape(node: VizNode): NodeShape;
|
|
@@ -19,6 +23,8 @@ export declare function getShapeBehavior(shape: NodeShape): ShapeBehavior<"circl
|
|
|
19
23
|
export declare function applyShapeGeometry(el: SVGElement, shape: NodeShape, pos: Vec2): void;
|
|
20
24
|
export declare function shapeSvgMarkup(shape: NodeShape, pos: Vec2, attrs: string): string;
|
|
21
25
|
export declare function computeNodeAnchor(node: VizNode, target: Vec2, anchor: AnchorMode): Vec2;
|
|
26
|
+
/** Resolve the perimeter point on a node's shape at the given angle (degrees, 0 = right, 90 = down). */
|
|
27
|
+
export declare function computeNodeAnchorAtAngle(node: VizNode, angleDeg: number): Vec2;
|
|
22
28
|
/**
|
|
23
29
|
* Return the default (implicit) ports for a node shape.
|
|
24
30
|
*
|
package/dist/shapes.js
CHANGED
|
@@ -30,6 +30,17 @@ export function effectiveShape(node) {
|
|
|
30
30
|
}
|
|
31
31
|
return shape;
|
|
32
32
|
}
|
|
33
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
34
|
+
/** Resolve a rect boundary point at the given angle (degrees). */
|
|
35
|
+
function rectAnchorAtAngle(pos, angleDeg, hw, hh) {
|
|
36
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
37
|
+
const dx = Math.cos(rad);
|
|
38
|
+
const dy = Math.sin(rad);
|
|
39
|
+
if (dx === 0 && dy === 0)
|
|
40
|
+
return { ...pos };
|
|
41
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
42
|
+
return { x: pos.x + dx * scale, y: pos.y + dy * scale };
|
|
43
|
+
}
|
|
33
44
|
function diamondPoints(pos, w, h) {
|
|
34
45
|
const hw = w / 2;
|
|
35
46
|
const hh = h / 2;
|
|
@@ -58,6 +69,13 @@ const circleBehavior = {
|
|
|
58
69
|
y: pos.y + dy * scale,
|
|
59
70
|
};
|
|
60
71
|
},
|
|
72
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
73
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
74
|
+
return {
|
|
75
|
+
x: pos.x + shape.r * Math.cos(rad),
|
|
76
|
+
y: pos.y + shape.r * Math.sin(rad),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
61
79
|
};
|
|
62
80
|
const rectBehavior = {
|
|
63
81
|
kind: 'rect',
|
|
@@ -87,6 +105,9 @@ const rectBehavior = {
|
|
|
87
105
|
y: pos.y + dy * scale,
|
|
88
106
|
};
|
|
89
107
|
},
|
|
108
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
109
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
110
|
+
},
|
|
90
111
|
};
|
|
91
112
|
const diamondBehavior = {
|
|
92
113
|
kind: 'diamond',
|
|
@@ -112,6 +133,18 @@ const diamondBehavior = {
|
|
|
112
133
|
y: pos.y + dy * scale,
|
|
113
134
|
};
|
|
114
135
|
},
|
|
136
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
137
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
138
|
+
const dx = Math.cos(rad);
|
|
139
|
+
const dy = Math.sin(rad);
|
|
140
|
+
if (dx === 0 && dy === 0)
|
|
141
|
+
return { ...pos };
|
|
142
|
+
const hw = shape.w / 2;
|
|
143
|
+
const hh = shape.h / 2;
|
|
144
|
+
const denom = Math.abs(dx) / hw + Math.abs(dy) / hh;
|
|
145
|
+
const scale = denom === 0 ? 0 : 1 / denom;
|
|
146
|
+
return { x: pos.x + dx * scale, y: pos.y + dy * scale };
|
|
147
|
+
},
|
|
115
148
|
};
|
|
116
149
|
function cylinderGeometry(shape, pos) {
|
|
117
150
|
const rx = shape.w / 2;
|
|
@@ -199,6 +232,9 @@ const cylinderBehavior = {
|
|
|
199
232
|
y: pos.y + dy * scale,
|
|
200
233
|
};
|
|
201
234
|
},
|
|
235
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
236
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
237
|
+
},
|
|
202
238
|
};
|
|
203
239
|
function hexagonPoints(pos, r, orientation) {
|
|
204
240
|
const pts = [];
|
|
@@ -238,6 +274,13 @@ const hexagonBehavior = {
|
|
|
238
274
|
y: pos.y + dy * scale,
|
|
239
275
|
};
|
|
240
276
|
},
|
|
277
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
278
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
279
|
+
return {
|
|
280
|
+
x: pos.x + shape.r * Math.cos(rad),
|
|
281
|
+
y: pos.y + shape.r * Math.sin(rad),
|
|
282
|
+
};
|
|
283
|
+
},
|
|
241
284
|
};
|
|
242
285
|
const ellipseBehavior = {
|
|
243
286
|
kind: 'ellipse',
|
|
@@ -262,6 +305,13 @@ const ellipseBehavior = {
|
|
|
262
305
|
y: pos.y + (shape.rx * shape.ry * dy) / denom,
|
|
263
306
|
};
|
|
264
307
|
},
|
|
308
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
309
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
310
|
+
return {
|
|
311
|
+
x: pos.x + shape.rx * Math.cos(rad),
|
|
312
|
+
y: pos.y + shape.ry * Math.sin(rad),
|
|
313
|
+
};
|
|
314
|
+
},
|
|
265
315
|
};
|
|
266
316
|
function arcPathD(shape, pos) {
|
|
267
317
|
const toRad = Math.PI / 180;
|
|
@@ -303,6 +353,13 @@ const arcBehavior = {
|
|
|
303
353
|
y: pos.y + dy * scale,
|
|
304
354
|
};
|
|
305
355
|
},
|
|
356
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
357
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
358
|
+
return {
|
|
359
|
+
x: pos.x + shape.r * Math.cos(rad),
|
|
360
|
+
y: pos.y + shape.r * Math.sin(rad),
|
|
361
|
+
};
|
|
362
|
+
},
|
|
306
363
|
};
|
|
307
364
|
function blockArrowPoints(shape, pos) {
|
|
308
365
|
const halfBody = shape.bodyWidth / 2;
|
|
@@ -360,6 +417,12 @@ const blockArrowBehavior = {
|
|
|
360
417
|
y: pos.y + dy * scale,
|
|
361
418
|
};
|
|
362
419
|
},
|
|
420
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
421
|
+
const dir = shape.direction ?? 'right';
|
|
422
|
+
const hw = dir === 'up' || dir === 'down' ? shape.headWidth / 2 : shape.length / 2;
|
|
423
|
+
const hh = dir === 'up' || dir === 'down' ? shape.length / 2 : shape.headWidth / 2;
|
|
424
|
+
return rectAnchorAtAngle(pos, angleDeg, hw, hh);
|
|
425
|
+
},
|
|
363
426
|
};
|
|
364
427
|
function calloutPathD(shape, pos) {
|
|
365
428
|
const hw = shape.w / 2;
|
|
@@ -461,6 +524,9 @@ const calloutBehavior = {
|
|
|
461
524
|
y: pos.y + dy * scale,
|
|
462
525
|
};
|
|
463
526
|
},
|
|
527
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
528
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
529
|
+
},
|
|
464
530
|
};
|
|
465
531
|
/**
|
|
466
532
|
* Compute the SVG path for a cloud shape that fits within a w×h bounding box
|
|
@@ -514,6 +580,12 @@ const cloudBehavior = {
|
|
|
514
580
|
y: pos.y + (a * b * dy) / denom,
|
|
515
581
|
};
|
|
516
582
|
},
|
|
583
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
584
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
585
|
+
const a = shape.w / 2;
|
|
586
|
+
const b = shape.h / 2;
|
|
587
|
+
return { x: pos.x + a * Math.cos(rad), y: pos.y + b * Math.sin(rad) };
|
|
588
|
+
},
|
|
517
589
|
};
|
|
518
590
|
function crossPoints(shape, pos) {
|
|
519
591
|
const hs = shape.size / 2;
|
|
@@ -556,6 +628,9 @@ const crossBehavior = {
|
|
|
556
628
|
y: pos.y + dy * scale,
|
|
557
629
|
};
|
|
558
630
|
},
|
|
631
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
632
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.size / 2, shape.size / 2);
|
|
633
|
+
},
|
|
559
634
|
};
|
|
560
635
|
function cubeVertices(shape, pos) {
|
|
561
636
|
const hw = shape.w / 2;
|
|
@@ -628,6 +703,10 @@ const cubeBehavior = {
|
|
|
628
703
|
y: pos.y + dy * scale,
|
|
629
704
|
};
|
|
630
705
|
},
|
|
706
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
707
|
+
const d = shape.depth ?? Math.round(shape.w * 0.2);
|
|
708
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2 + d / 2, shape.h / 2 + d / 2);
|
|
709
|
+
},
|
|
631
710
|
};
|
|
632
711
|
const pathBehavior = {
|
|
633
712
|
kind: 'path',
|
|
@@ -654,6 +733,9 @@ const pathBehavior = {
|
|
|
654
733
|
y: pos.y + dy * scale,
|
|
655
734
|
};
|
|
656
735
|
},
|
|
736
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
737
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
738
|
+
},
|
|
657
739
|
};
|
|
658
740
|
function documentPathD(shape, pos) {
|
|
659
741
|
const hw = shape.w / 2;
|
|
@@ -693,6 +775,9 @@ const documentBehavior = {
|
|
|
693
775
|
y: pos.y + dy * scale,
|
|
694
776
|
};
|
|
695
777
|
},
|
|
778
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
779
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
780
|
+
},
|
|
696
781
|
};
|
|
697
782
|
function noteVertices(shape, pos) {
|
|
698
783
|
const hw = shape.w / 2;
|
|
@@ -748,6 +833,9 @@ const noteBehavior = {
|
|
|
748
833
|
y: pos.y + dy * scale,
|
|
749
834
|
};
|
|
750
835
|
},
|
|
836
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
837
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
838
|
+
},
|
|
751
839
|
};
|
|
752
840
|
function parallelogramPoints(shape, pos) {
|
|
753
841
|
const hw = shape.w / 2;
|
|
@@ -785,6 +873,10 @@ const parallelogramBehavior = {
|
|
|
785
873
|
y: pos.y + dy * scale,
|
|
786
874
|
};
|
|
787
875
|
},
|
|
876
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
877
|
+
const sk = shape.skew ?? Math.round(shape.w * 0.2);
|
|
878
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2 + sk / 2, shape.h / 2);
|
|
879
|
+
},
|
|
788
880
|
};
|
|
789
881
|
function starPoints(shape, pos) {
|
|
790
882
|
const n = shape.points;
|
|
@@ -820,6 +912,13 @@ const starBehavior = {
|
|
|
820
912
|
y: pos.y + dy * scale,
|
|
821
913
|
};
|
|
822
914
|
},
|
|
915
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
916
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
917
|
+
return {
|
|
918
|
+
x: pos.x + shape.outerR * Math.cos(rad),
|
|
919
|
+
y: pos.y + shape.outerR * Math.sin(rad),
|
|
920
|
+
};
|
|
921
|
+
},
|
|
823
922
|
};
|
|
824
923
|
function trapezoidPoints(shape, pos) {
|
|
825
924
|
const htw = shape.topW / 2;
|
|
@@ -855,6 +954,10 @@ const trapezoidBehavior = {
|
|
|
855
954
|
y: pos.y + dy * scale,
|
|
856
955
|
};
|
|
857
956
|
},
|
|
957
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
958
|
+
const hw = Math.max(shape.topW, shape.bottomW) / 2;
|
|
959
|
+
return rectAnchorAtAngle(pos, angleDeg, hw, shape.h / 2);
|
|
960
|
+
},
|
|
858
961
|
};
|
|
859
962
|
function trianglePoints(shape, pos) {
|
|
860
963
|
const hw = shape.w / 2;
|
|
@@ -910,6 +1013,9 @@ const triangleBehavior = {
|
|
|
910
1013
|
y: pos.y + dy * scale,
|
|
911
1014
|
};
|
|
912
1015
|
},
|
|
1016
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
1017
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
1018
|
+
},
|
|
913
1019
|
};
|
|
914
1020
|
const imageBehavior = {
|
|
915
1021
|
kind: 'image',
|
|
@@ -955,6 +1061,9 @@ const imageBehavior = {
|
|
|
955
1061
|
y: pos.y + dy * scale,
|
|
956
1062
|
};
|
|
957
1063
|
},
|
|
1064
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
1065
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
1066
|
+
},
|
|
958
1067
|
};
|
|
959
1068
|
const iconBehavior = {
|
|
960
1069
|
kind: 'icon',
|
|
@@ -1006,6 +1115,9 @@ const iconBehavior = {
|
|
|
1006
1115
|
y: pos.y + dy * scale,
|
|
1007
1116
|
};
|
|
1008
1117
|
},
|
|
1118
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
1119
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
1120
|
+
},
|
|
1009
1121
|
};
|
|
1010
1122
|
const svgBehavior = {
|
|
1011
1123
|
kind: 'svg',
|
|
@@ -1044,6 +1156,9 @@ const svgBehavior = {
|
|
|
1044
1156
|
y: pos.y + dy * scale,
|
|
1045
1157
|
};
|
|
1046
1158
|
},
|
|
1159
|
+
anchorAtAngle(pos, angleDeg, shape) {
|
|
1160
|
+
return rectAnchorAtAngle(pos, angleDeg, shape.w / 2, shape.h / 2);
|
|
1161
|
+
},
|
|
1047
1162
|
};
|
|
1048
1163
|
const shapeBehaviorRegistry = {
|
|
1049
1164
|
circle: circleBehavior,
|
|
@@ -1089,6 +1204,13 @@ export function computeNodeAnchor(node, target, anchor) {
|
|
|
1089
1204
|
const behavior = getShapeBehavior(shape);
|
|
1090
1205
|
return behavior.anchorBoundary(pos, target, shape);
|
|
1091
1206
|
}
|
|
1207
|
+
/** Resolve the perimeter point on a node's shape at the given angle (degrees, 0 = right, 90 = down). */
|
|
1208
|
+
export function computeNodeAnchorAtAngle(node, angleDeg) {
|
|
1209
|
+
const pos = effectivePos(node);
|
|
1210
|
+
const shape = effectiveShape(node);
|
|
1211
|
+
const behavior = getShapeBehavior(shape);
|
|
1212
|
+
return behavior.anchorAtAngle(pos, angleDeg, shape);
|
|
1213
|
+
}
|
|
1092
1214
|
// ── Connection Ports ────────────────────────────────────────────────────────
|
|
1093
1215
|
/**
|
|
1094
1216
|
* Return the default (implicit) ports for a node shape.
|
package/dist/types.d.ts
CHANGED
|
@@ -402,8 +402,18 @@ export type EdgeRouting = 'straight' | 'curved' | 'orthogonal';
|
|
|
402
402
|
export type EdgeMarkerType = 'none' | 'arrow' | 'arrowOpen' | 'diamond' | 'diamondOpen' | 'circle' | 'circleOpen' | 'square' | 'bar' | 'halfArrow';
|
|
403
403
|
export interface VizEdge {
|
|
404
404
|
id: string;
|
|
405
|
-
|
|
406
|
-
|
|
405
|
+
/** Source node id. Optional for dangling edges (use `fromAt` instead). */
|
|
406
|
+
from?: string;
|
|
407
|
+
/** Target node id. Optional for dangling edges (use `toAt` instead). */
|
|
408
|
+
to?: string;
|
|
409
|
+
/** Free-endpoint coordinate for the source end (when `from` is omitted). */
|
|
410
|
+
fromAt?: Vec2;
|
|
411
|
+
/** Free-endpoint coordinate for the target end (when `to` is omitted). */
|
|
412
|
+
toAt?: Vec2;
|
|
413
|
+
/** Angle (degrees) for the source perimeter anchor. 0 = right, 90 = down. */
|
|
414
|
+
fromAngle?: number;
|
|
415
|
+
/** Angle (degrees) for the target perimeter anchor. 0 = right, 90 = down. */
|
|
416
|
+
toAngle?: number;
|
|
407
417
|
/** Arbitrary consumer-defined metadata associated with the edge. */
|
|
408
418
|
meta?: Record<string, unknown>;
|
|
409
419
|
/** @deprecated Use `labels` for multi-position support. Kept for backwards compatibility. */
|
|
@@ -642,6 +652,18 @@ export interface NodeOptions {
|
|
|
642
652
|
export interface EdgeOptions {
|
|
643
653
|
/** Custom edge id (defaults to `"from->to"`). */
|
|
644
654
|
id?: string;
|
|
655
|
+
/** Source node id. Use with `danglingEdge()` to attach one end to a node. */
|
|
656
|
+
from?: string;
|
|
657
|
+
/** Target node id. Use with `danglingEdge()` to attach one end to a node. */
|
|
658
|
+
to?: string;
|
|
659
|
+
/** Free-endpoint coordinate for the source end (when `from` is omitted). */
|
|
660
|
+
fromAt?: Vec2;
|
|
661
|
+
/** Free-endpoint coordinate for the target end (when `to` is omitted). */
|
|
662
|
+
toAt?: Vec2;
|
|
663
|
+
/** Angle (degrees) for the source perimeter anchor. 0 = right, 90 = down. */
|
|
664
|
+
fromAngle?: number;
|
|
665
|
+
/** Angle (degrees) for the target perimeter anchor. 0 = right, 90 = down. */
|
|
666
|
+
toAngle?: number;
|
|
645
667
|
routing?: EdgeRouting;
|
|
646
668
|
waypoints?: Vec2[];
|
|
647
669
|
/** Convenience for arrow markers. `true` = markerEnd arrow, `'both'` = both ends. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vizcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visualization",
|