vizcraft 1.5.0 → 1.7.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/README.md +28 -3
- package/dist/edges/index.d.ts +1 -0
- package/dist/edges/index.js +1 -0
- package/dist/edges/resolveEdgeGeometry.d.ts +54 -0
- package/dist/edges/resolveEdgeGeometry.js +73 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ports/equidistant/index.d.ts +1 -1
- package/dist/ports/equidistant/index.js +1 -1
- package/dist/ports/equidistant/registry.d.ts +23 -1
- package/dist/ports/equidistant/registry.js +41 -1
- package/dist/ports/equidistant/strategies/circle.js +2 -2
- package/dist/ports/equidistant/strategies/diamond.js +7 -1
- package/dist/ports/equidistant/strategies/hexagon.js +21 -1
- package/dist/ports/equidistant/strategies/parallelogram.js +2 -1
- package/dist/ports/equidistant/strategies/rect.js +2 -1
- package/dist/ports/equidistant/strategies/trapezoid.js +2 -1
- package/dist/ports/equidistant/strategies/triangle.js +13 -1
- package/dist/ports/equidistant/utils.d.ts +35 -5
- package/dist/ports/equidistant/utils.js +68 -8
- package/dist/shapes/geometry.d.ts +5 -0
- package/dist/shapes/geometry.js +17 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# vizcraft
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#98](https://github.com/ChipiKaf/vizcraft/pull/98) [`b513c21`](https://github.com/ChipiKaf/vizcraft/commit/b513c21308d115b0634cf63a0c63b3708684a172) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Add `resolveEdgeGeometry(scene, edgeId)` convenience function that resolves all rendered geometry for an edge in a single call — node lookup, self-loop detection, port/angle/boundary anchors, waypoints, routing, SVG path, midpoint, and label positions. Also exports `resolveEdgeGeometryFromData` for batch processing with a pre-built node map.
|
|
8
|
+
|
|
9
|
+
## 1.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`8020286`](https://github.com/ChipiKaf/vizcraft/commit/8020286d96d210f932a493184d820111e8c9ac7d) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Added fromProt and toPort to attach edges to specific edges. Updated documentation to follow Diátaxis structure
|
|
14
|
+
|
|
3
15
|
## 1.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -309,11 +309,16 @@ b.edge('srv', 'db').fromPort('out-1').toPort('in').arrow();
|
|
|
309
309
|
// Default ports (no .port() needed) — every shape has built-in ports
|
|
310
310
|
b.edge('a', 'b').fromPort('right').toPort('left').arrow();
|
|
311
311
|
|
|
312
|
-
// Equidistant port distribution —
|
|
313
|
-
import { getEquidistantPorts, toNodePorts } from 'vizcraft';
|
|
314
|
-
const ports = getEquidistantPorts({ kind: '
|
|
312
|
+
// Equidistant port distribution — stable, location-based IDs
|
|
313
|
+
import { getEquidistantPorts, toNodePorts, findPortNearest } from 'vizcraft';
|
|
314
|
+
const ports = getEquidistantPorts({ kind: 'rect', w: 120, h: 60 }, 8);
|
|
315
|
+
// → [{ id: 'top-0', … }, { id: 'top-1', … }, { id: 'right-0', … }, …]
|
|
315
316
|
const nodePorts = toNodePorts(ports); // → NodePort[] ready for node.ports
|
|
316
317
|
|
|
318
|
+
// Snap to nearest port (node-local coordinates)
|
|
319
|
+
const nearest = findPortNearest(node, clickX - node.pos.x, clickY - node.pos.y);
|
|
320
|
+
if (nearest) b.edge('a', 'b').toPort(nearest.id);
|
|
321
|
+
|
|
317
322
|
// Dangling edges — one or both endpoints at a free coordinate
|
|
318
323
|
b.danglingEdge('preview').from('srv').toAt({ x: 300, y: 200 }).arrow().dashed();
|
|
319
324
|
|
|
@@ -321,6 +326,26 @@ b.danglingEdge('preview').from('srv').toAt({ x: 300, y: 200 }).arrow().dashed();
|
|
|
321
326
|
b.danglingEdge('e1', { from: 'srv', toAt: { x: 300, y: 200 }, arrow: true });
|
|
322
327
|
```
|
|
323
328
|
|
|
329
|
+
#### Resolving edge geometry
|
|
330
|
+
|
|
331
|
+
`resolveEdgeGeometry(scene, edgeId)` resolves all rendered geometry for an edge in a single call — anchor points, SVG path, midpoint, label positions, waypoints, and self-loop detection:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import { resolveEdgeGeometry } from 'vizcraft';
|
|
335
|
+
|
|
336
|
+
const geo = resolveEdgeGeometry(scene, 'edge-1');
|
|
337
|
+
if (!geo) return; // edge not found or unresolvable
|
|
338
|
+
|
|
339
|
+
overlayPath.setAttribute('d', geo.d); // SVG path
|
|
340
|
+
positionToolbar(geo.mid); // midpoint
|
|
341
|
+
drawHandle(geo.startAnchor); // source anchor
|
|
342
|
+
drawHandle(geo.endAnchor); // target anchor
|
|
343
|
+
geo.waypoints.forEach(drawDot); // waypoints
|
|
344
|
+
if (geo.isSelfLoop) {
|
|
345
|
+
/* ... */
|
|
346
|
+
} // self-loop flag
|
|
347
|
+
```
|
|
348
|
+
|
|
324
349
|
| Method | Description |
|
|
325
350
|
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
326
351
|
| `.straight()` | Direct line (default). With waypoints → polyline. |
|
package/dist/edges/index.d.ts
CHANGED
package/dist/edges/index.js
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convenience function that resolves the full rendered geometry for an edge
|
|
3
|
+
* in a single call. Handles node lookup, dangling edges, self-loop detection,
|
|
4
|
+
* port/angle/boundary anchors, waypoints, and routing.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { Vec2, VizScene, VizEdge, VizNode } from '../types';
|
|
9
|
+
import type { EdgePathResult } from './paths';
|
|
10
|
+
/**
|
|
11
|
+
* Fully resolved edge geometry returned by {@link resolveEdgeGeometry}.
|
|
12
|
+
*
|
|
13
|
+
* Extends `EdgePathResult` with extra convenience fields so consumers
|
|
14
|
+
* never need to orchestrate multiple helpers manually.
|
|
15
|
+
*/
|
|
16
|
+
export interface ResolvedEdgeGeometry extends EdgePathResult {
|
|
17
|
+
/** Source anchor position (alias of `start` from EdgePathResult, ~15% along path). */
|
|
18
|
+
startAnchor: Vec2;
|
|
19
|
+
/** Target anchor position (alias of `end` from EdgePathResult, ~85% along path). */
|
|
20
|
+
endAnchor: Vec2;
|
|
21
|
+
/** Waypoints used for the path (empty array when none). */
|
|
22
|
+
waypoints: Vec2[];
|
|
23
|
+
/** Whether this edge is a self-loop (same source and target node). */
|
|
24
|
+
isSelfLoop: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve all rendered geometry for a single edge in one call.
|
|
28
|
+
*
|
|
29
|
+
* Returns `null` when:
|
|
30
|
+
* - The edge id is not found in the scene.
|
|
31
|
+
* - A referenced `from` / `to` node id does not exist.
|
|
32
|
+
* - Both endpoints are missing (no `from`/`to` and no `fromAt`/`toAt`).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { resolveEdgeGeometry } from 'vizcraft';
|
|
37
|
+
*
|
|
38
|
+
* const geo = resolveEdgeGeometry(scene, 'edge-1');
|
|
39
|
+
* if (!geo) return;
|
|
40
|
+
* overlay.setAttribute('d', geo.d);
|
|
41
|
+
* positionToolbar(geo.mid);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveEdgeGeometry(scene: VizScene, edgeId: string): ResolvedEdgeGeometry | null;
|
|
45
|
+
/**
|
|
46
|
+
* Lower-level helper: resolve geometry from an edge + a node lookup map.
|
|
47
|
+
*
|
|
48
|
+
* Useful when the caller already has a `Map` built (e.g. inside a render loop
|
|
49
|
+
* processing many edges).
|
|
50
|
+
*
|
|
51
|
+
* @internal exported for advanced consumers and testing — prefer
|
|
52
|
+
* {@link resolveEdgeGeometry} for typical usage.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveEdgeGeometryFromData(edge: VizEdge, nodesById: Map<string, VizNode>): ResolvedEdgeGeometry | null;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convenience function that resolves the full rendered geometry for an edge
|
|
3
|
+
* in a single call. Handles node lookup, dangling edges, self-loop detection,
|
|
4
|
+
* port/angle/boundary anchors, waypoints, and routing.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { computeEdgeEndpoints, computeEdgePath, computeSelfLoop, } from './paths';
|
|
9
|
+
// ── Implementation ──────────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Resolve all rendered geometry for a single edge in one call.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` when:
|
|
14
|
+
* - The edge id is not found in the scene.
|
|
15
|
+
* - A referenced `from` / `to` node id does not exist.
|
|
16
|
+
* - Both endpoints are missing (no `from`/`to` and no `fromAt`/`toAt`).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { resolveEdgeGeometry } from 'vizcraft';
|
|
21
|
+
*
|
|
22
|
+
* const geo = resolveEdgeGeometry(scene, 'edge-1');
|
|
23
|
+
* if (!geo) return;
|
|
24
|
+
* overlay.setAttribute('d', geo.d);
|
|
25
|
+
* positionToolbar(geo.mid);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function resolveEdgeGeometry(scene, edgeId) {
|
|
29
|
+
const edge = scene.edges.find((e) => e.id === edgeId);
|
|
30
|
+
if (!edge)
|
|
31
|
+
return null;
|
|
32
|
+
const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
|
|
33
|
+
return resolveEdgeGeometryFromData(edge, nodesById);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lower-level helper: resolve geometry from an edge + a node lookup map.
|
|
37
|
+
*
|
|
38
|
+
* Useful when the caller already has a `Map` built (e.g. inside a render loop
|
|
39
|
+
* processing many edges).
|
|
40
|
+
*
|
|
41
|
+
* @internal exported for advanced consumers and testing — prefer
|
|
42
|
+
* {@link resolveEdgeGeometry} for typical usage.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveEdgeGeometryFromData(edge, nodesById) {
|
|
45
|
+
// ── Node lookup (null-safe for dangling edges) ──────────────────────────
|
|
46
|
+
const startNode = edge.from ? (nodesById.get(edge.from) ?? null) : null;
|
|
47
|
+
const endNode = edge.to ? (nodesById.get(edge.to) ?? null) : null;
|
|
48
|
+
// Bail out if a referenced node id doesn't exist.
|
|
49
|
+
if (edge.from && !startNode)
|
|
50
|
+
return null;
|
|
51
|
+
if (edge.to && !endNode)
|
|
52
|
+
return null;
|
|
53
|
+
// Bail out if both endpoints are entirely unresolvable.
|
|
54
|
+
if (!startNode && !edge.fromAt && !endNode && !edge.toAt)
|
|
55
|
+
return null;
|
|
56
|
+
// ── Self-loop detection ─────────────────────────────────────────────────
|
|
57
|
+
const isSelfLoop = !!(startNode && endNode && startNode === endNode);
|
|
58
|
+
let pathResult;
|
|
59
|
+
if (isSelfLoop) {
|
|
60
|
+
pathResult = computeSelfLoop(startNode, edge);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const endpoints = computeEdgeEndpoints(startNode, endNode, edge);
|
|
64
|
+
pathResult = computeEdgePath(endpoints.start, endpoints.end, edge.routing, edge.waypoints);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...pathResult,
|
|
68
|
+
startAnchor: pathResult.start,
|
|
69
|
+
endAnchor: pathResult.end,
|
|
70
|
+
waypoints: edge.waypoints ?? [],
|
|
71
|
+
isSelfLoop,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './overlays/builder';
|
|
|
8
8
|
export * from './edges/paths';
|
|
9
9
|
export * from './edges/labels';
|
|
10
10
|
export * from './edges/styles';
|
|
11
|
+
export * from './edges/resolveEdgeGeometry';
|
|
11
12
|
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, computeNodeAnchorAtAngle, } from './shapes/geometry';
|
|
12
13
|
export * from './animation/spec';
|
|
13
14
|
export * from './animation/builder';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './overlays/builder';
|
|
|
8
8
|
export * from './edges/paths';
|
|
9
9
|
export * from './edges/labels';
|
|
10
10
|
export * from './edges/styles';
|
|
11
|
+
export * from './edges/resolveEdgeGeometry';
|
|
11
12
|
export { getDefaultPorts, getNodePorts, findPort, resolvePortPosition, computeNodeAnchorAtAngle, } from './shapes/geometry';
|
|
12
13
|
export * from './animation/spec';
|
|
13
14
|
export * from './animation/builder';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { EquidistantPort, PerimeterStrategy } from './types';
|
|
2
|
-
export { getEquidistantPorts, toNodePorts, registerPerimeterStrategy, } from './registry';
|
|
2
|
+
export { getEquidistantPorts, toNodePorts, findPortNearest, registerPerimeterStrategy, } from './registry';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { getEquidistantPorts, toNodePorts, registerPerimeterStrategy, } from './registry';
|
|
1
|
+
export { getEquidistantPorts, toNodePorts, findPortNearest, registerPerimeterStrategy, } from './registry';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodePort, NodeShape } from '../../types';
|
|
1
|
+
import type { NodePort, NodeShape, VizNode } from '../../types';
|
|
2
2
|
import type { EquidistantPort, PerimeterStrategy } from './types';
|
|
3
3
|
/** Register (or replace) a {@link PerimeterStrategy} for a shape kind. */
|
|
4
4
|
export declare function registerPerimeterStrategy<K extends NodeShape['kind']>(strategy: PerimeterStrategy<K>): void;
|
|
@@ -7,9 +7,31 @@ export declare function registerPerimeterStrategy<K extends NodeShape['kind']>(s
|
|
|
7
7
|
* Delegates to a registered {@link PerimeterStrategy} or falls back to
|
|
8
8
|
* a bounding-box rectangle approximation.
|
|
9
9
|
*
|
|
10
|
+
* Port IDs are **location-based** and stable across count changes:
|
|
11
|
+
* - Polygon shapes with named sides: `{side}-{index}` (e.g. `top-0`,
|
|
12
|
+
* `right-1`).
|
|
13
|
+
* - Curved / complex shapes: `{angleBucket}-{index}` (e.g. `0-0`,
|
|
14
|
+
* `270-1`).
|
|
15
|
+
*
|
|
10
16
|
* @param shape - The node shape specification.
|
|
11
17
|
* @param count - Number of ports (uses a shape-specific default when omitted).
|
|
12
18
|
*/
|
|
13
19
|
export declare function getEquidistantPorts(shape: NodeShape, count?: number): EquidistantPort[];
|
|
14
20
|
/** Convert equidistant ports to `NodePort[]` with `offset` and `direction`. */
|
|
15
21
|
export declare function toNodePorts(ports: readonly EquidistantPort[]): NodePort[];
|
|
22
|
+
/**
|
|
23
|
+
* Return the port on `node` closest to the given point (in **node-local
|
|
24
|
+
* coordinates**, i.e. relative to the node center).
|
|
25
|
+
*
|
|
26
|
+
* Uses the node's effective ports ({@link getNodePorts} — explicit
|
|
27
|
+
* `node.ports` when set, otherwise shape defaults).
|
|
28
|
+
*
|
|
29
|
+
* Returns `undefined` when the node has no ports.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const port = findPortNearest(node, clickX - node.x, clickY - node.y);
|
|
34
|
+
* if (port) edgeBuilder.toPort(port.id);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function findPortNearest(node: VizNode, x: number, y: number): NodePort | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getNodePorts } from '../../shapes/geometry';
|
|
1
2
|
import { walkPolygonEquidistant } from './utils';
|
|
2
3
|
import { builtInStrategies } from './strategies';
|
|
3
4
|
const FALLBACK_COUNT = 8;
|
|
@@ -63,6 +64,7 @@ function shapeBoundingBox(shape) {
|
|
|
63
64
|
return { hw: 0, hh: 0 };
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
const BBOX_SIDES = ['top', 'right', 'bottom', 'left'];
|
|
66
68
|
function boundingBoxFallback(shape, count) {
|
|
67
69
|
const { hw, hh } = shapeBoundingBox(shape);
|
|
68
70
|
return walkPolygonEquidistant([
|
|
@@ -70,13 +72,19 @@ function boundingBoxFallback(shape, count) {
|
|
|
70
72
|
{ x: hw, y: -hh },
|
|
71
73
|
{ x: hw, y: hh },
|
|
72
74
|
{ x: -hw, y: hh },
|
|
73
|
-
], count);
|
|
75
|
+
], count, BBOX_SIDES);
|
|
74
76
|
}
|
|
75
77
|
/**
|
|
76
78
|
* Compute N equidistant points along a shape's perimeter by arc length.
|
|
77
79
|
* Delegates to a registered {@link PerimeterStrategy} or falls back to
|
|
78
80
|
* a bounding-box rectangle approximation.
|
|
79
81
|
*
|
|
82
|
+
* Port IDs are **location-based** and stable across count changes:
|
|
83
|
+
* - Polygon shapes with named sides: `{side}-{index}` (e.g. `top-0`,
|
|
84
|
+
* `right-1`).
|
|
85
|
+
* - Curved / complex shapes: `{angleBucket}-{index}` (e.g. `0-0`,
|
|
86
|
+
* `270-1`).
|
|
87
|
+
*
|
|
80
88
|
* @param shape - The node shape specification.
|
|
81
89
|
* @param count - Number of ports (uses a shape-specific default when omitted).
|
|
82
90
|
*/
|
|
@@ -102,3 +110,35 @@ export function toNodePorts(ports) {
|
|
|
102
110
|
direction: p.angle,
|
|
103
111
|
}));
|
|
104
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Return the port on `node` closest to the given point (in **node-local
|
|
115
|
+
* coordinates**, i.e. relative to the node center).
|
|
116
|
+
*
|
|
117
|
+
* Uses the node's effective ports ({@link getNodePorts} — explicit
|
|
118
|
+
* `node.ports` when set, otherwise shape defaults).
|
|
119
|
+
*
|
|
120
|
+
* Returns `undefined` when the node has no ports.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const port = findPortNearest(node, clickX - node.x, clickY - node.y);
|
|
125
|
+
* if (port) edgeBuilder.toPort(port.id);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function findPortNearest(node, x, y) {
|
|
129
|
+
const ports = getNodePorts(node);
|
|
130
|
+
if (ports.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
let nearest;
|
|
133
|
+
let bestDist = Infinity;
|
|
134
|
+
for (const port of ports) {
|
|
135
|
+
const dx = port.offset.x - x;
|
|
136
|
+
const dy = port.offset.y - y;
|
|
137
|
+
const d = dx * dx + dy * dy;
|
|
138
|
+
if (d < bestDist) {
|
|
139
|
+
bestDist = d;
|
|
140
|
+
nearest = port;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return nearest;
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RAD, portFromPoint } from '../utils';
|
|
1
|
+
import { RAD, portFromPoint, assignAngleBucketIds } from '../utils';
|
|
2
2
|
function circleEquidistant(r, count) {
|
|
3
3
|
const ports = [];
|
|
4
4
|
for (let i = 0; i < count; i++) {
|
|
@@ -6,7 +6,7 @@ function circleEquidistant(r, count) {
|
|
|
6
6
|
const aRad = aDeg * RAD;
|
|
7
7
|
ports.push(portFromPoint({ x: r * Math.cos(aRad), y: r * Math.sin(aRad) }, i, i / count));
|
|
8
8
|
}
|
|
9
|
-
return ports;
|
|
9
|
+
return assignAngleBucketIds(ports);
|
|
10
10
|
}
|
|
11
11
|
export const circleStrategy = {
|
|
12
12
|
kind: 'circle',
|
|
@@ -9,4 +9,10 @@ function diamondVertices(w, h) {
|
|
|
9
9
|
{ x: -hw, y: 0 },
|
|
10
10
|
];
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
const DIAMOND_SIDES = [
|
|
13
|
+
'top-right',
|
|
14
|
+
'bottom-right',
|
|
15
|
+
'bottom-left',
|
|
16
|
+
'top-left',
|
|
17
|
+
];
|
|
18
|
+
export const diamondStrategy = polygonStrategy('diamond', 4, (s) => diamondVertices(s.w, s.h), () => DIAMOND_SIDES);
|
|
@@ -8,4 +8,24 @@ function hexagonVertices(r, orientation) {
|
|
|
8
8
|
}
|
|
9
9
|
return verts;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
function hexagonSideLabels(orientation) {
|
|
12
|
+
if (orientation === 'pointy') {
|
|
13
|
+
return [
|
|
14
|
+
'top-right',
|
|
15
|
+
'right',
|
|
16
|
+
'bottom-right',
|
|
17
|
+
'bottom-left',
|
|
18
|
+
'left',
|
|
19
|
+
'top-left',
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
'bottom-right',
|
|
24
|
+
'bottom',
|
|
25
|
+
'bottom-left',
|
|
26
|
+
'top-left',
|
|
27
|
+
'top',
|
|
28
|
+
'top-right',
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export const hexagonStrategy = polygonStrategy('hexagon', 6, (s) => hexagonVertices(s.r, s.orientation ?? 'pointy'), (s) => hexagonSideLabels(s.orientation ?? 'pointy'));
|
|
@@ -10,4 +10,5 @@ function parallelogramVertices(w, h, skew) {
|
|
|
10
10
|
{ x: -hw - half, y: hh },
|
|
11
11
|
];
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const PARALLELOGRAM_SIDES = ['top', 'right', 'bottom', 'left'];
|
|
14
|
+
export const parallelogramStrategy = polygonStrategy('parallelogram', 8, (s) => parallelogramVertices(s.w, s.h, s.skew ?? Math.round(s.w * 0.2)), () => PARALLELOGRAM_SIDES);
|
|
@@ -9,4 +9,5 @@ function rectVertices(w, h) {
|
|
|
9
9
|
{ x: -hw, y: hh },
|
|
10
10
|
];
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
const RECT_SIDES = ['top', 'right', 'bottom', 'left'];
|
|
13
|
+
export const rectStrategy = polygonStrategy('rect', 8, (s) => rectVertices(s.w, s.h), () => RECT_SIDES);
|
|
@@ -10,4 +10,5 @@ function trapezoidVertices(topW, bottomW, h) {
|
|
|
10
10
|
{ x: -hbw, y: hh },
|
|
11
11
|
];
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const TRAPEZOID_SIDES = ['top', 'right', 'bottom', 'left'];
|
|
14
|
+
export const trapezoidStrategy = polygonStrategy('trapezoid', 8, (s) => trapezoidVertices(s.topW, s.bottomW, s.h), () => TRAPEZOID_SIDES);
|
|
@@ -29,4 +29,16 @@ function triangleVertices(w, h, direction) {
|
|
|
29
29
|
];
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
function triangleSideLabels(direction) {
|
|
33
|
+
switch (direction) {
|
|
34
|
+
case 'up':
|
|
35
|
+
return ['right', 'bottom', 'left'];
|
|
36
|
+
case 'down':
|
|
37
|
+
return ['left', 'top', 'right'];
|
|
38
|
+
case 'left':
|
|
39
|
+
return ['top', 'right', 'bottom'];
|
|
40
|
+
case 'right':
|
|
41
|
+
return ['bottom', 'left', 'top'];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const triangleStrategy = polygonStrategy('triangle', 6, (s) => triangleVertices(s.w, s.h, s.direction ?? 'up'), (s) => triangleSideLabels(s.direction ?? 'up'));
|
|
@@ -4,15 +4,45 @@ export declare const DEG: number;
|
|
|
4
4
|
export declare const RAD: number;
|
|
5
5
|
export declare const ARC_SAMPLES = 720;
|
|
6
6
|
export declare function portFromPoint(pt: Vec2, i: number, t: number): EquidistantPort;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Assign angle-bucket IDs to ports based on their angle from center.
|
|
9
|
+
*
|
|
10
|
+
* Each port is bucketed into a 90° quadrant (0, 90, 180, 270) and given
|
|
11
|
+
* an index within that quadrant. For example, a port at 45° becomes `0-0`
|
|
12
|
+
* (first port in the 0°–90° quadrant).
|
|
13
|
+
*
|
|
14
|
+
* This scheme is stable across count changes — ports near the same angle
|
|
15
|
+
* keep the same ID prefix regardless of total port count.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assignAngleBucketIds(ports: readonly EquidistantPort[]): EquidistantPort[];
|
|
18
|
+
/**
|
|
19
|
+
* Place `count` equidistant points along a closed polygon by arc length.
|
|
20
|
+
*
|
|
21
|
+
* When `sideLabels` is provided (one label per edge), each port receives a
|
|
22
|
+
* location-based ID in the format `{label}-{indexWithinSide}` (e.g.
|
|
23
|
+
* `top-0`, `right-1`). Without labels, ports fall back to sequential
|
|
24
|
+
* `p0`–`pN` IDs.
|
|
25
|
+
*/
|
|
26
|
+
export declare function walkPolygonEquidistant(vertices: Vec2[], count: number, sideLabels?: readonly string[]): EquidistantPort[];
|
|
27
|
+
/**
|
|
28
|
+
* Create a polygon strategy from a vertex extractor.
|
|
29
|
+
*
|
|
30
|
+
* When `extractSideLabels` is provided, ports receive location-based IDs
|
|
31
|
+
* (e.g. `top-0`, `right-1`). Otherwise, angle-bucket IDs are assigned
|
|
32
|
+
* (e.g. `0-0`, `90-1`).
|
|
33
|
+
*/
|
|
10
34
|
export declare function polygonStrategy<K extends NodeShape['kind']>(kind: K, defaultCount: number | ((shape: Extract<NodeShape, {
|
|
11
35
|
kind: K;
|
|
12
36
|
}>) => number), extractVertices: (shape: Extract<NodeShape, {
|
|
13
37
|
kind: K;
|
|
14
|
-
}>) => Vec2[]
|
|
15
|
-
|
|
38
|
+
}>) => Vec2[], extractSideLabels?: (shape: Extract<NodeShape, {
|
|
39
|
+
kind: K;
|
|
40
|
+
}>) => readonly string[]): PerimeterStrategy<K>;
|
|
41
|
+
/**
|
|
42
|
+
* Create a curved-shape strategy from a perimeter sampler.
|
|
43
|
+
*
|
|
44
|
+
* Ports receive angle-bucket IDs (e.g. `0-0`, `90-1`).
|
|
45
|
+
*/
|
|
16
46
|
export declare function sampledCurveStrategy<K extends NodeShape['kind']>(kind: K, defaultCount: number, samplePerimeter: (shape: Extract<NodeShape, {
|
|
17
47
|
kind: K;
|
|
18
48
|
}>) => Vec2[]): PerimeterStrategy<K>;
|
|
@@ -22,8 +22,34 @@ export function portFromPoint(pt, i, t) {
|
|
|
22
22
|
y: pt.y,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Assign angle-bucket IDs to ports based on their angle from center.
|
|
27
|
+
*
|
|
28
|
+
* Each port is bucketed into a 90° quadrant (0, 90, 180, 270) and given
|
|
29
|
+
* an index within that quadrant. For example, a port at 45° becomes `0-0`
|
|
30
|
+
* (first port in the 0°–90° quadrant).
|
|
31
|
+
*
|
|
32
|
+
* This scheme is stable across count changes — ports near the same angle
|
|
33
|
+
* keep the same ID prefix regardless of total port count.
|
|
34
|
+
*/
|
|
35
|
+
export function assignAngleBucketIds(ports) {
|
|
36
|
+
const bucketCounts = new Map();
|
|
37
|
+
return ports.map((p) => {
|
|
38
|
+
const bucket = Math.floor(normalizeAngle(p.angle) / 90) * 90;
|
|
39
|
+
const idx = bucketCounts.get(bucket) ?? 0;
|
|
40
|
+
bucketCounts.set(bucket, idx + 1);
|
|
41
|
+
return { ...p, id: `${bucket}-${idx}` };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Place `count` equidistant points along a closed polygon by arc length.
|
|
46
|
+
*
|
|
47
|
+
* When `sideLabels` is provided (one label per edge), each port receives a
|
|
48
|
+
* location-based ID in the format `{label}-{indexWithinSide}` (e.g.
|
|
49
|
+
* `top-0`, `right-1`). Without labels, ports fall back to sequential
|
|
50
|
+
* `p0`–`pN` IDs.
|
|
51
|
+
*/
|
|
52
|
+
export function walkPolygonEquidistant(vertices, count, sideLabels) {
|
|
27
53
|
const n = vertices.length;
|
|
28
54
|
if (n === 0 || count <= 0)
|
|
29
55
|
return [];
|
|
@@ -38,6 +64,8 @@ export function walkPolygonEquidistant(vertices, count) {
|
|
|
38
64
|
const segLen = perimeter / count;
|
|
39
65
|
const ports = [];
|
|
40
66
|
let edgeIdx = 0;
|
|
67
|
+
// Track per-side port counts for side-based IDs
|
|
68
|
+
const sideCounts = sideLabels ? new Array(n).fill(0) : [];
|
|
41
69
|
for (let i = 0; i < count; i++) {
|
|
42
70
|
const target = i * segLen;
|
|
43
71
|
while (edgeIdx < n - 1 && cumDist[edgeIdx + 1] <= target)
|
|
@@ -48,7 +76,24 @@ export function walkPolygonEquidistant(vertices, count) {
|
|
|
48
76
|
const frac = edgeLen > 0 ? (target - edgeStart) / edgeLen : 0;
|
|
49
77
|
const a = vertices[edgeIdx];
|
|
50
78
|
const b = vertices[(edgeIdx + 1) % n];
|
|
51
|
-
|
|
79
|
+
const pt = lerp(a, b, frac);
|
|
80
|
+
let id;
|
|
81
|
+
if (sideLabels) {
|
|
82
|
+
const sideLabel = sideLabels[edgeIdx];
|
|
83
|
+
const sideIdx = sideCounts[edgeIdx];
|
|
84
|
+
sideCounts[edgeIdx] = sideIdx + 1;
|
|
85
|
+
id = `${sideLabel}-${sideIdx}`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
id = `p${i}`;
|
|
89
|
+
}
|
|
90
|
+
ports.push({
|
|
91
|
+
id,
|
|
92
|
+
angle: normalizeAngle(angleDeg(pt)),
|
|
93
|
+
t: target / perimeter,
|
|
94
|
+
x: pt.x,
|
|
95
|
+
y: pt.y,
|
|
96
|
+
});
|
|
52
97
|
}
|
|
53
98
|
return ports;
|
|
54
99
|
}
|
|
@@ -90,19 +135,34 @@ function walkSampledCurveEquidistant(pts, count) {
|
|
|
90
135
|
}
|
|
91
136
|
return ports;
|
|
92
137
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Create a polygon strategy from a vertex extractor.
|
|
140
|
+
*
|
|
141
|
+
* When `extractSideLabels` is provided, ports receive location-based IDs
|
|
142
|
+
* (e.g. `top-0`, `right-1`). Otherwise, angle-bucket IDs are assigned
|
|
143
|
+
* (e.g. `0-0`, `90-1`).
|
|
144
|
+
*/
|
|
145
|
+
export function polygonStrategy(kind, defaultCount, extractVertices, extractSideLabels) {
|
|
95
146
|
return {
|
|
96
147
|
kind,
|
|
97
148
|
defaultCount,
|
|
98
|
-
computePorts: (shape, count) =>
|
|
149
|
+
computePorts: (shape, count) => {
|
|
150
|
+
const vertices = extractVertices(shape);
|
|
151
|
+
const labels = extractSideLabels?.(shape);
|
|
152
|
+
const ports = walkPolygonEquidistant(vertices, count, labels);
|
|
153
|
+
return labels ? ports : assignAngleBucketIds(ports);
|
|
154
|
+
},
|
|
99
155
|
};
|
|
100
156
|
}
|
|
101
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Create a curved-shape strategy from a perimeter sampler.
|
|
159
|
+
*
|
|
160
|
+
* Ports receive angle-bucket IDs (e.g. `0-0`, `90-1`).
|
|
161
|
+
*/
|
|
102
162
|
export function sampledCurveStrategy(kind, defaultCount, samplePerimeter) {
|
|
103
163
|
return {
|
|
104
164
|
kind,
|
|
105
165
|
defaultCount,
|
|
106
|
-
computePorts: (shape, count) => walkSampledCurveEquidistant(samplePerimeter(shape), count),
|
|
166
|
+
computePorts: (shape, count) => assignAngleBucketIds(walkSampledCurveEquidistant(samplePerimeter(shape), count)),
|
|
107
167
|
};
|
|
108
168
|
}
|
|
@@ -40,6 +40,11 @@ export declare function getDefaultPorts(shape: NodeShape): NodePort[];
|
|
|
40
40
|
export declare function getNodePorts(node: VizNode): NodePort[];
|
|
41
41
|
/**
|
|
42
42
|
* Find a port on a node by its id. Returns `undefined` if not found.
|
|
43
|
+
*
|
|
44
|
+
* **Legacy compatibility:** if `portId` matches the old sequential format
|
|
45
|
+
* (`p0`, `p1`, …) and no port with that exact ID exists, the port at
|
|
46
|
+
* that numeric index is returned instead. This eases migration from the
|
|
47
|
+
* previous `p0`–`pN` naming to the new location-based IDs.
|
|
43
48
|
*/
|
|
44
49
|
export declare function findPort(node: VizNode, portId: string): NodePort | undefined;
|
|
45
50
|
/**
|
package/dist/shapes/geometry.js
CHANGED
|
@@ -1372,9 +1372,25 @@ export function getNodePorts(node) {
|
|
|
1372
1372
|
}
|
|
1373
1373
|
/**
|
|
1374
1374
|
* Find a port on a node by its id. Returns `undefined` if not found.
|
|
1375
|
+
*
|
|
1376
|
+
* **Legacy compatibility:** if `portId` matches the old sequential format
|
|
1377
|
+
* (`p0`, `p1`, …) and no port with that exact ID exists, the port at
|
|
1378
|
+
* that numeric index is returned instead. This eases migration from the
|
|
1379
|
+
* previous `p0`–`pN` naming to the new location-based IDs.
|
|
1375
1380
|
*/
|
|
1376
1381
|
export function findPort(node, portId) {
|
|
1377
|
-
|
|
1382
|
+
const ports = getNodePorts(node);
|
|
1383
|
+
const exact = ports.find((p) => p.id === portId);
|
|
1384
|
+
if (exact)
|
|
1385
|
+
return exact;
|
|
1386
|
+
// Legacy fallback: treat 'p3' as "port at index 3"
|
|
1387
|
+
const legacy = /^p(\d+)$/.exec(portId);
|
|
1388
|
+
if (legacy) {
|
|
1389
|
+
const idx = Number(legacy[1]);
|
|
1390
|
+
if (idx >= 0 && idx < ports.length)
|
|
1391
|
+
return ports[idx];
|
|
1392
|
+
}
|
|
1393
|
+
return undefined;
|
|
1378
1394
|
}
|
|
1379
1395
|
/**
|
|
1380
1396
|
* Resolve a port to an absolute position (node center + port offset).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vizcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|