@oh-just-another/scene 0.57.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/a11y.d.ts +23 -0
  6. package/dist/a11y.d.ts.map +1 -0
  7. package/dist/a11y.js +37 -0
  8. package/dist/a11y.js.map +1 -0
  9. package/dist/anchors.d.ts +120 -0
  10. package/dist/anchors.d.ts.map +1 -0
  11. package/dist/anchors.js +322 -0
  12. package/dist/anchors.js.map +1 -0
  13. package/dist/annotation-geometry.d.ts +11 -0
  14. package/dist/annotation-geometry.d.ts.map +1 -0
  15. package/dist/annotation-geometry.js +19 -0
  16. package/dist/annotation-geometry.js.map +1 -0
  17. package/dist/annotation.d.ts +32 -0
  18. package/dist/annotation.d.ts.map +1 -0
  19. package/dist/annotation.js +2 -0
  20. package/dist/annotation.js.map +1 -0
  21. package/dist/constants.d.ts +186 -0
  22. package/dist/constants.d.ts.map +1 -0
  23. package/dist/constants.js +186 -0
  24. package/dist/constants.js.map +1 -0
  25. package/dist/container.d.ts +112 -0
  26. package/dist/container.d.ts.map +1 -0
  27. package/dist/container.js +227 -0
  28. package/dist/container.js.map +1 -0
  29. package/dist/diff.d.ts +22 -0
  30. package/dist/diff.d.ts.map +1 -0
  31. package/dist/diff.js +18 -0
  32. package/dist/diff.js.map +1 -0
  33. package/dist/edge-curve.d.ts +37 -0
  34. package/dist/edge-curve.d.ts.map +1 -0
  35. package/dist/edge-curve.js +72 -0
  36. package/dist/edge-curve.js.map +1 -0
  37. package/dist/edge-geometry.d.ts +96 -0
  38. package/dist/edge-geometry.d.ts.map +1 -0
  39. package/dist/edge-geometry.js +467 -0
  40. package/dist/edge-geometry.js.map +1 -0
  41. package/dist/edge.d.ts +206 -0
  42. package/dist/edge.d.ts.map +1 -0
  43. package/dist/edge.js +2 -0
  44. package/dist/edge.js.map +1 -0
  45. package/dist/elbow-link.d.ts +23 -0
  46. package/dist/elbow-link.d.ts.map +1 -0
  47. package/dist/elbow-link.js +419 -0
  48. package/dist/elbow-link.js.map +1 -0
  49. package/dist/elbow-router.d.ts +20 -0
  50. package/dist/elbow-router.d.ts.map +1 -0
  51. package/dist/elbow-router.js +218 -0
  52. package/dist/elbow-router.js.map +1 -0
  53. package/dist/file.d.ts +41 -0
  54. package/dist/file.d.ts.map +1 -0
  55. package/dist/file.js +13 -0
  56. package/dist/file.js.map +1 -0
  57. package/dist/heading.d.ts +42 -0
  58. package/dist/heading.d.ts.map +1 -0
  59. package/dist/heading.js +77 -0
  60. package/dist/heading.js.map +1 -0
  61. package/dist/index.d.ts +47 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +40 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/layer.d.ts +18 -0
  66. package/dist/layer.d.ts.map +1 -0
  67. package/dist/layer.js +2 -0
  68. package/dist/layer.js.map +1 -0
  69. package/dist/layout-registry.d.ts +35 -0
  70. package/dist/layout-registry.d.ts.map +1 -0
  71. package/dist/layout-registry.js +13 -0
  72. package/dist/layout-registry.js.map +1 -0
  73. package/dist/layout.d.ts +128 -0
  74. package/dist/layout.d.ts.map +1 -0
  75. package/dist/layout.js +453 -0
  76. package/dist/layout.js.map +1 -0
  77. package/dist/operations.d.ts +32 -0
  78. package/dist/operations.d.ts.map +1 -0
  79. package/dist/operations.js +105 -0
  80. package/dist/operations.js.map +1 -0
  81. package/dist/outline.d.ts +36 -0
  82. package/dist/outline.d.ts.map +1 -0
  83. package/dist/outline.js +129 -0
  84. package/dist/outline.js.map +1 -0
  85. package/dist/patch.d.ts +57 -0
  86. package/dist/patch.d.ts.map +1 -0
  87. package/dist/patch.js +47 -0
  88. package/dist/patch.js.map +1 -0
  89. package/dist/queries.d.ts +115 -0
  90. package/dist/queries.d.ts.map +1 -0
  91. package/dist/queries.js +403 -0
  92. package/dist/queries.js.map +1 -0
  93. package/dist/render-bounds.d.ts +36 -0
  94. package/dist/render-bounds.d.ts.map +1 -0
  95. package/dist/render-bounds.js +36 -0
  96. package/dist/render-bounds.js.map +1 -0
  97. package/dist/scene.d.ts +80 -0
  98. package/dist/scene.d.ts.map +1 -0
  99. package/dist/scene.js +129 -0
  100. package/dist/scene.js.map +1 -0
  101. package/dist/shape.d.ts +306 -0
  102. package/dist/shape.d.ts.map +1 -0
  103. package/dist/shape.js +192 -0
  104. package/dist/shape.js.map +1 -0
  105. package/dist/snap.d.ts +88 -0
  106. package/dist/snap.d.ts.map +1 -0
  107. package/dist/snap.js +128 -0
  108. package/dist/snap.js.map +1 -0
  109. package/dist/spatial.d.ts +38 -0
  110. package/dist/spatial.d.ts.map +1 -0
  111. package/dist/spatial.js +95 -0
  112. package/dist/spatial.js.map +1 -0
  113. package/dist/style.d.ts +87 -0
  114. package/dist/style.d.ts.map +1 -0
  115. package/dist/style.js +43 -0
  116. package/dist/style.js.map +1 -0
  117. package/dist/text-measure.d.ts +29 -0
  118. package/dist/text-measure.d.ts.map +1 -0
  119. package/dist/text-measure.js +8 -0
  120. package/dist/text-measure.js.map +1 -0
  121. package/dist/three-way-merge.d.ts +59 -0
  122. package/dist/three-way-merge.d.ts.map +1 -0
  123. package/dist/three-way-merge.js +108 -0
  124. package/dist/three-way-merge.js.map +1 -0
  125. package/dist/viewport.d.ts +78 -0
  126. package/dist/viewport.d.ts.map +1 -0
  127. package/dist/viewport.js +62 -0
  128. package/dist/viewport.js.map +1 -0
  129. package/package.json +54 -0
@@ -0,0 +1,72 @@
1
+ import { CURVE_CATMULL_TENSION, CURVE_END_TANGENT_MAX_PX, CURVE_END_TANGENT_RATIO, CURVE_FLATTEN_SEGMENTS, } from "./constants.js";
2
+ const req = (v) => {
3
+ if (v === undefined)
4
+ throw new Error("packages/scene: index out of range");
5
+ return v;
6
+ };
7
+ /**
8
+ * Cubic bezier from `from` to `to` that LEAVES `from` along `dirFrom` and
9
+ * ENTERS `to` along `-dirTo` (both unit outward directions). This makes a
10
+ * connector exit/enter perpendicular to an element's edge (flowchart look),
11
+ * instead of always bowing to one fixed side. Control-arm length scales with
12
+ * the endpoint distance (capped).
13
+ */
14
+ export const cubicWithEndTangents = (from, to, dirFrom, dirTo) => {
15
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
16
+ const d = Math.min(dist * CURVE_END_TANGENT_RATIO, CURVE_END_TANGENT_MAX_PX);
17
+ return {
18
+ c1: { x: from.x + dirFrom.x * d, y: from.y + dirFrom.y * d },
19
+ c2: { x: to.x + dirTo.x * d, y: to.y + dirTo.y * d },
20
+ to,
21
+ };
22
+ };
23
+ /**
24
+ * Catmull-Rom spline through `pts` as cubic bezier segments. Each segment
25
+ * Pi→Pi+1 uses tangents from the neighbouring points; endpoints duplicate
26
+ * themselves. The curve passes through every point with no corners. Used for
27
+ * a waypointed curve (the bends define the shape).
28
+ */
29
+ export const catmullRomBeziers = (pts) => {
30
+ const segs = [];
31
+ const k = CURVE_CATMULL_TENSION;
32
+ for (let i = 0; i < pts.length - 1; i++) {
33
+ const p1 = req(pts[i]);
34
+ const p2 = req(pts[i + 1]);
35
+ const p0 = pts[i - 1] ?? p1;
36
+ const p3 = pts[i + 2] ?? p2;
37
+ segs.push({
38
+ c1: { x: p1.x + (p2.x - p0.x) / k, y: p1.y + (p2.y - p0.y) / k },
39
+ c2: { x: p2.x - (p3.x - p1.x) / k, y: p2.y - (p3.y - p1.y) / k },
40
+ to: p2,
41
+ });
42
+ }
43
+ return segs;
44
+ };
45
+ /** Point on a cubic bezier at parameter `t` (0..1). de Casteljau / Bernstein. */
46
+ export const cubicAt = (p0, c1, c2, p1, t) => {
47
+ const u = 1 - t;
48
+ const a = u * u * u;
49
+ const b = 3 * u * u * t;
50
+ const c = 3 * u * t * t;
51
+ const d = t * t * t;
52
+ return {
53
+ x: a * p0.x + b * c1.x + c * c2.x + d * p1.x,
54
+ y: a * p0.y + b * c1.y + c * c2.y + d * p1.y,
55
+ };
56
+ };
57
+ /**
58
+ * Flatten a `start` + cubic-segment list into a dense point list (for
59
+ * hit-testing / bounds). Includes `start` then `perSegment` samples per cubic.
60
+ */
61
+ export const flattenSegments = (start, segments, perSegment = CURVE_FLATTEN_SEGMENTS) => {
62
+ const out = [start];
63
+ let prev = start;
64
+ for (const s of segments) {
65
+ for (let i = 1; i <= perSegment; i++) {
66
+ out.push(cubicAt(prev, s.c1, s.c2, s.to, i / perSegment));
67
+ }
68
+ prev = s.to;
69
+ }
70
+ return out;
71
+ };
72
+ //# sourceMappingURL=edge-curve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-curve.js","sourceRoot":"","sources":["../src/edge-curve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import type { Bounds, Vec2 } from "@oh-just-another/types";
2
+ import { type BezierSegment } from "./edge-curve.js";
3
+ import type { Link, LinkEndpoint } from "./edge.js";
4
+ import type { Scene } from "./scene.js";
5
+ /**
6
+ * --- Self-loop geometry ---
7
+ *
8
+ * A link whose `from` and `to` both bind to the SAME element. Routed OUTSIDE the
9
+ * element: curved → an arc, orthogonal/straight → a rectangular loop. Without
10
+ * this both ends collapse to (nearly) the same point and the link renders as a
11
+ * flat line on/across the shape.
12
+ */
13
+ interface SelfLoopSpec {
14
+ readonly p1: Vec2;
15
+ readonly n1: Vec2;
16
+ readonly p2: Vec2;
17
+ readonly n2: Vec2;
18
+ readonly bounds: Bounds;
19
+ }
20
+ /**
21
+ * Resolve a self-loop's two outline exit points + outward normals, or `null`
22
+ * when the link isn't a self-loop. When both ends land on the same point
23
+ * (centre / floating / same anchor) they're spread along the chosen side
24
+ * (default top) so the loop has width.
25
+ */
26
+ export declare const getSelfLoopSpec: (scene: Scene, edge: Link) => SelfLoopSpec | null;
27
+ /**
28
+ * Resolve a single `LinkEndpoint` to a world-space point.
29
+ *
30
+ * `kind: "point"` returns the stored position as-is. `kind: "anchor"`
31
+ * looks up the referenced shape and resolves the anchor through
32
+ * `getAnchorWorld` — picking up the shape's current position / rotation /
33
+ * scale automatically.
34
+ *
35
+ * Returns `null` when an `anchor` endpoint references a shape id that
36
+ * isn't (or no longer is) in the scene. Callers usually drop the edge
37
+ * from the render pass in that case.
38
+ *
39
+ * `toward` is only consulted for `floating` endpoints — it's the world
40
+ * point of the *other* endpoint, which the floating side aims at. When
41
+ * omitted, a floating endpoint resolves to the shape's centre (used as a
42
+ * provisional value before the partner is known; see `getLinkPath`).
43
+ */
44
+ export declare const getLinkEndpointWorld: (scene: Scene, endpoint: LinkEndpoint, toward?: Vec2) => Vec2 | null;
45
+ /**
46
+ * Produce the full polyline an edge draws, in world coordinates:
47
+ * `[from, ...waypoints, to]` for `straight` routing; an axis-aligned
48
+ * elbow for `orthogonal`; the two endpoints for `bezier` (control points
49
+ * are derived by the renderer).
50
+ *
51
+ * Returns `null` if either endpoint can't be resolved. Honours an
52
+ * explicit `edge.waypoints` array on every routing — hosts can pre-route
53
+ * an edge with custom bends.
54
+ */
55
+ export declare const getLinkPath: (scene: Scene, edge: Link) => readonly Vec2[] | null;
56
+ /**
57
+ * Cubic-bezier representation of a curved (bezier) edge: the start point and
58
+ * the list of cubic segments. A no-waypoint span is a single cubic whose
59
+ * control arms follow the endpoints' edge normals (exits/enters perpendicular
60
+ * to the element edge — flowchart look); a waypointed span is a Catmull-Rom
61
+ * spline through the bends. Returns `null` for non-bezier / unresolvable.
62
+ */
63
+ export declare const getLinkCurveSegments: (scene: Scene, edge: Link) => {
64
+ start: Vec2;
65
+ segments: BezierSegment[];
66
+ } | null;
67
+ /**
68
+ * Hit-test / bounds polyline for an edge: the routed path, except a curved
69
+ * (bezier) edge is flattened to follow the drawn arc (so clicking the
70
+ * visible curve — which bows away from the straight chord — actually hits).
71
+ * Returns `null` when the path is unresolvable.
72
+ */
73
+ export declare const getLinkCurvePoints: (scene: Scene, edge: Link) => readonly Vec2[] | null;
74
+ /**
75
+ * Per-span "add a waypoint" handle positions for a selected link's bend
76
+ * editing — one handle per logical span of `[from, ...waypoints, to]`, index i
77
+ * being the span between point i and i+1 (what `beginWaypointDrag(i, insert)`
78
+ * expects). The handle sits at the VISUAL middle of its span:
79
+ * - bezier — the curve point at `t=0.5` of that span's cubic, so the handle
80
+ * lands ON the drawn arc (not the straight chord beside it);
81
+ * - straight — the chord midpoint (the span IS straight).
82
+ * Returns `null` for orthogonal (elbow) links — their segment handles are
83
+ * computed from the routed path by the caller — or when unresolvable.
84
+ */
85
+ export declare const getLinkWaypointMidpoints: (scene: Scene, edge: Link) => Vec2[] | null;
86
+ /**
87
+ * Topmost edge under `worldPoint`, or `null` if none is within
88
+ * `threshold` world-pixels of any segment. Iterates edges in layer
89
+ * order; later layers (rendered on top) win when paths overlap.
90
+ *
91
+ * Curved (bezier) edges are tested against the flattened curve (via
92
+ * `getLinkCurvePoints`) so the clickable line matches the drawn arc.
93
+ */
94
+ export declare const findLinkAt: (scene: Scene, worldPoint: Vec2, threshold?: number) => Link | null;
95
+ export {};
96
+ //# sourceMappingURL=edge-geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-geometry.d.ts","sourceRoot":"","sources":["../src/edge-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAa,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAItE,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAIpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAQxC;;;;;;;GAOG;AACH,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AA2BD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,KAAK,EAAE,MAAM,IAAI,KAAG,YAAY,GAAG,IA6BzE,CAAC;AAsGF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,KAAK,EACZ,UAAU,YAAY,EACtB,SAAS,IAAI,KACZ,IAAI,GAAG,IAQT,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK,EAAE,MAAM,IAAI,KAAG,SAAS,IAAI,EAAE,GAAG,IA8FxE,CAAC;AA+DF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,KAAK,EACZ,MAAM,IAAI,KACT;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAe/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,KAAK,EAAE,MAAM,IAAI,KAAG,SAAS,IAAI,EAAE,GAAG,IAK/E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,KAAK,EAAE,MAAM,IAAI,KAAG,IAAI,EAAE,GAAG,IAuB5E,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,EAAE,YAAY,IAAI,EAAE,kBAAa,KAAG,IAAI,GAAG,IAkBjF,CAAC"}
@@ -0,0 +1,467 @@
1
+ import { intersect } from "@oh-just-another/math";
2
+ import { getAnchorOutwardNormal, getAnchorWorld } from "./anchors.js";
3
+ import { SELF_LOOP_CURVE_ARM_FACTOR, SELF_LOOP_SIZE, SELF_LOOP_SPREAD } from "./constants.js";
4
+ import { catmullRomBeziers, cubicAt, cubicWithEndTangents, flattenSegments, } from "./edge-curve.js";
5
+ import { headingForEdgePoint } from "./heading.js";
6
+ import { getOutlinePoint, getOutlineSampler } from "./outline.js";
7
+ import { getElementWorldBounds } from "./shape.js";
8
+ import { getElement } from "./queries.js";
9
+ const req = (v) => {
10
+ if (v === undefined)
11
+ throw new Error("packages/scene: index out of range");
12
+ return v;
13
+ };
14
+ const endpointElementOf = (ep) => ep.kind === "point" ? null : ep.elementId;
15
+ /** Snap a unit vector to the dominant axis (corner anchors give diagonals). */
16
+ const axisSnap = (n) => Math.abs(n.x) >= Math.abs(n.y) ? { x: Math.sign(n.x) || 0, y: 0 } : { x: 0, y: Math.sign(n.y) };
17
+ const outwardNormalFor = (shape, ep, p) => {
18
+ if (ep.kind === "anchor") {
19
+ const n = axisSnap(getAnchorOutwardNormal(shape, ep.anchor));
20
+ if (n.x !== 0 || n.y !== 0)
21
+ return n;
22
+ }
23
+ const h = axisSnap(headingForEdgePoint(shape, p));
24
+ return h.x !== 0 || h.y !== 0 ? h : { x: 0, y: -1 };
25
+ };
26
+ const edgeMidpoint = (b, n) => {
27
+ const cx = b.x + b.width / 2;
28
+ const cy = b.y + b.height / 2;
29
+ if (n.y < 0)
30
+ return { x: cx, y: b.y };
31
+ if (n.y > 0)
32
+ return { x: cx, y: b.y + b.height };
33
+ if (n.x < 0)
34
+ return { x: b.x, y: cy };
35
+ return { x: b.x + b.width, y: cy };
36
+ };
37
+ /**
38
+ * Resolve a self-loop's two outline exit points + outward normals, or `null`
39
+ * when the link isn't a self-loop. When both ends land on the same point
40
+ * (centre / floating / same anchor) they're spread along the chosen side
41
+ * (default top) so the loop has width.
42
+ */
43
+ export const getSelfLoopSpec = (scene, edge) => {
44
+ const el = endpointElementOf(edge.from);
45
+ if (!el || el !== endpointElementOf(edge.to))
46
+ return null;
47
+ const shape = getElement(scene, el);
48
+ if (!shape)
49
+ return null;
50
+ const bounds = getElementWorldBounds(shape);
51
+ const w1 = getLinkEndpointWorld(scene, edge.from);
52
+ const w2 = getLinkEndpointWorld(scene, edge.to);
53
+ if (!w1 || !w2)
54
+ return null;
55
+ let p1 = w1;
56
+ let p2 = w2;
57
+ let n1 = outwardNormalFor(shape, edge.from, p1);
58
+ let n2 = outwardNormalFor(shape, edge.to, p2);
59
+ if (Math.hypot(p2.x - p1.x, p2.y - p1.y) < 1) {
60
+ // Degenerate: both ends at one point → loop on n1's side (default top),
61
+ // two points spread symmetrically along that edge.
62
+ const side = n1.x !== 0 || n1.y !== 0 ? n1 : { x: 0, y: -1 };
63
+ n1 = side;
64
+ n2 = side;
65
+ const t = { x: -side.y, y: side.x }; // tangent along the edge
66
+ const sideLen = side.x !== 0 ? bounds.height : bounds.width;
67
+ const half = Math.min(SELF_LOOP_SPREAD, sideLen / 3);
68
+ const mid = edgeMidpoint(bounds, side);
69
+ p1 = { x: mid.x - t.x * half, y: mid.y - t.y * half };
70
+ p2 = { x: mid.x + t.x * half, y: mid.y + t.y * half };
71
+ }
72
+ return { p1, n1, p2, n2, bounds };
73
+ };
74
+ /** Orthogonal/straight self-loop polyline `[p1, …outside…, p2]`. */
75
+ const selfLoopPolyline = (spec) => {
76
+ const L = SELF_LOOP_SIZE;
77
+ const { p1, n1, p2, n2, bounds } = spec;
78
+ const a1 = { x: p1.x + n1.x * L, y: p1.y + n1.y * L };
79
+ const a2 = { x: p2.x + n2.x * L, y: p2.y + n2.y * L };
80
+ const same = n1.x === n2.x && n1.y === n2.y;
81
+ const opposite = n1.x === -n2.x && n1.y === -n2.y;
82
+ if (same) {
83
+ // Staple: both stubs on the same outward level.
84
+ return [p1, a1, a2, p2];
85
+ }
86
+ if (opposite) {
87
+ // Wrap around one side (the +x side for a vertical pair, +y for horizontal).
88
+ if (n1.x === 0) {
89
+ const outX = bounds.x + bounds.width + L;
90
+ return [p1, a1, { x: outX, y: a1.y }, { x: outX, y: a2.y }, a2, p2];
91
+ }
92
+ const outY = bounds.y + bounds.height + L;
93
+ return [p1, a1, { x: a1.x, y: outY }, { x: a2.x, y: outY }, a2, p2];
94
+ }
95
+ // Perpendicular: route through the outside corner.
96
+ const corner = { x: n1.y !== 0 ? a2.x : a1.x, y: n1.y !== 0 ? a1.y : a2.y };
97
+ return [p1, a1, corner, a2, p2];
98
+ };
99
+ /** Curved self-loop: a single cubic that bows out along both normals. */
100
+ const selfLoopCurve = (spec) => {
101
+ const k = SELF_LOOP_SIZE * SELF_LOOP_CURVE_ARM_FACTOR;
102
+ const { p1, n1, p2, n2 } = spec;
103
+ return {
104
+ start: p1,
105
+ segments: [
106
+ {
107
+ c1: { x: p1.x + n1.x * k, y: p1.y + n1.y * k },
108
+ c2: { x: p2.x + n2.x * k, y: p2.y + n2.y * k },
109
+ to: p2,
110
+ },
111
+ ],
112
+ };
113
+ };
114
+ /**
115
+ * World-space centre of a shape's bounding box. Origin of the ray used to
116
+ * resolve a `floating` endpoint.
117
+ */
118
+ const shapeCentre = (shape) => {
119
+ const b = getElementWorldBounds(shape);
120
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
121
+ };
122
+ /**
123
+ * Number of segments the outline is sampled into when intersecting it with
124
+ * the floating-endpoint ray. 96 is smooth enough for ellipses at high zoom
125
+ * without being a hot-loop cost (resolved once per edge per frame).
126
+ */
127
+ const FLOATING_OUTLINE_SAMPLES = 96;
128
+ /**
129
+ * Resolve a `floating` endpoint: the point where the shape's outline is
130
+ * crossed by the ray from the shape's centre toward `toward` (the other
131
+ * endpoint). Slides along the perimeter as either shape moves, always
132
+ * entering from the side facing the partner.
133
+ *
134
+ * Falls back to the centre when the ray is degenerate (partner at the
135
+ * centre) or the shape has no outline sampler. Picks the crossing nearest
136
+ * to `toward` so a convex shape connects on its outward-facing side.
137
+ */
138
+ const floatingOutlineWorld = (shape, toward) => {
139
+ const centre = shapeCentre(shape);
140
+ const dx = toward.x - centre.x;
141
+ const dy = toward.y - centre.y;
142
+ if (Math.abs(dx) < 1e-6 && Math.abs(dy) < 1e-6)
143
+ return centre;
144
+ const sampler = getOutlineSampler(shape.type);
145
+ if (!sampler)
146
+ return centre;
147
+ // Extend the ray well past `toward` so the crossing is always inside the
148
+ // tested segment even when `toward` sits on or inside the outline.
149
+ const len = Math.hypot(dx, dy);
150
+ const far = { x: centre.x + (dx / len) * 1e6, y: centre.y + (dy / len) * 1e6 };
151
+ let best = null;
152
+ let bestDist = Infinity;
153
+ let prev = getOutlinePoint(shape, 0);
154
+ for (let i = 1; i <= FLOATING_OUTLINE_SAMPLES; i++) {
155
+ const cur = getOutlinePoint(shape, i / FLOATING_OUTLINE_SAMPLES);
156
+ const hit = intersect.segmentSegment(centre, far, prev, cur);
157
+ if (hit) {
158
+ const d = Math.hypot(hit.x - toward.x, hit.y - toward.y);
159
+ if (d < bestDist) {
160
+ bestDist = d;
161
+ best = hit;
162
+ }
163
+ }
164
+ prev = cur;
165
+ }
166
+ return best ?? centre;
167
+ };
168
+ /**
169
+ * Resolve a single `LinkEndpoint` to a world-space point.
170
+ *
171
+ * `kind: "point"` returns the stored position as-is. `kind: "anchor"`
172
+ * looks up the referenced shape and resolves the anchor through
173
+ * `getAnchorWorld` — picking up the shape's current position / rotation /
174
+ * scale automatically.
175
+ *
176
+ * Returns `null` when an `anchor` endpoint references a shape id that
177
+ * isn't (or no longer is) in the scene. Callers usually drop the edge
178
+ * from the render pass in that case.
179
+ *
180
+ * `toward` is only consulted for `floating` endpoints — it's the world
181
+ * point of the *other* endpoint, which the floating side aims at. When
182
+ * omitted, a floating endpoint resolves to the shape's centre (used as a
183
+ * provisional value before the partner is known; see `getLinkPath`).
184
+ */
185
+ export const getLinkEndpointWorld = (scene, endpoint, toward) => {
186
+ if (endpoint.kind === "point")
187
+ return endpoint.position;
188
+ const shape = getElement(scene, endpoint.elementId);
189
+ if (!shape)
190
+ return null;
191
+ if (endpoint.kind === "anchor")
192
+ return getAnchorWorld(shape, endpoint.anchor);
193
+ if (endpoint.kind === "floating")
194
+ return floatingOutlineWorld(shape, toward ?? shapeCentre(shape));
195
+ return getOutlinePoint(shape, endpoint.ratio);
196
+ };
197
+ /**
198
+ * Produce the full polyline an edge draws, in world coordinates:
199
+ * `[from, ...waypoints, to]` for `straight` routing; an axis-aligned
200
+ * elbow for `orthogonal`; the two endpoints for `bezier` (control points
201
+ * are derived by the renderer).
202
+ *
203
+ * Returns `null` if either endpoint can't be resolved. Honours an
204
+ * explicit `edge.waypoints` array on every routing — hosts can pre-route
205
+ * an edge with custom bends.
206
+ */
207
+ export const getLinkPath = (scene, edge) => {
208
+ // Self-loop (both ends on the same element): route a loop OUTSIDE the shape
209
+ // (orthogonal/straight → rectangular loop) instead of a flat line. Curved
210
+ // self-loops are handled in `getLinkCurveSegments`.
211
+ const loop = getSelfLoopSpec(scene, edge);
212
+ if (loop)
213
+ return selfLoopPolyline(loop);
214
+ // Two-pass resolve so a `floating` endpoint can aim at its partner.
215
+ // Pass 1: provisional points (floating → its own centre). Pass 2:
216
+ // re-resolve floating ends toward the partner's provisional point. When
217
+ // both ends float, each aims at the other shape's centre — stable.
218
+ const fromProvisional = getLinkEndpointWorld(scene, edge.from);
219
+ if (!fromProvisional)
220
+ return null;
221
+ const toProvisional = getLinkEndpointWorld(scene, edge.to);
222
+ if (!toProvisional)
223
+ return null;
224
+ const from = edge.from.kind === "floating"
225
+ ? (getLinkEndpointWorld(scene, edge.from, toProvisional) ?? fromProvisional)
226
+ : fromProvisional;
227
+ const to = edge.to.kind === "floating"
228
+ ? (getLinkEndpointWorld(scene, edge.to, fromProvisional) ?? toProvisional)
229
+ : toProvisional;
230
+ const routing = edge.routing ?? "straight";
231
+ if (routing !== "orthogonal") {
232
+ // straight / bezier: honour user-placed waypoints (free bend points).
233
+ return [from, ...(edge.waypoints ?? []), to];
234
+ }
235
+ // Orthogonal (elbow): the path is the router's output, stored on the edge
236
+ // as `routedPoints` (corner points between from and to). Elbow points are
237
+ // NOT user-placed — segments must stay axis-aligned — so `waypoints` are
238
+ // ignored here. A *defined* `routedPoints` means the editor has routed this
239
+ // edge — use it verbatim (empty = a straight run). Only fall back to the
240
+ // side-aware heuristic when the edge has never been routed (undefined),
241
+ // e.g. headless `getLinkPath` outside the editor's reroute pass.
242
+ if (edge.routedPoints !== undefined) {
243
+ return [from, ...edge.routedPoints, to];
244
+ }
245
+ // Orthogonal heuristic fallback — modern-style elbow with side-aware
246
+ // stubs.
247
+ // When either endpoint is anchored to a named side (top / right /
248
+ // bottom / left), we know which axis the edge should *exit* on,
249
+ // so we add a small stub in that direction before bending toward
250
+ // the other endpoint. Corner / center anchors and free `point`
251
+ // endpoints fall back to the longest-axis-first heuristic.
252
+ const fromDir = exitDirectionFor(edge.from);
253
+ const toDir = exitDirectionFor(edge.to);
254
+ const stub = Math.min(40, Math.max(8, Math.abs(to.x - from.x) / 4, Math.abs(to.y - from.y) / 4));
255
+ if (fromDir || toDir) {
256
+ const path = [from];
257
+ let cursorFrom = from;
258
+ const cursorTo = to;
259
+ if (fromDir) {
260
+ cursorFrom = { x: from.x + fromDir.x * stub, y: from.y + fromDir.y * stub };
261
+ path.push(cursorFrom);
262
+ }
263
+ let toStubPoint = null;
264
+ if (toDir) {
265
+ toStubPoint = { x: to.x + toDir.x * stub, y: to.y + toDir.y * stub };
266
+ }
267
+ // Bend between cursorFrom and (toStubPoint ?? to).
268
+ const end = toStubPoint ?? cursorTo;
269
+ const dx = end.x - cursorFrom.x;
270
+ const dy = end.y - cursorFrom.y;
271
+ // Pick bend axis based on the originating stub direction when
272
+ // available — horizontal stub bends vertical first to align,
273
+ // and vice-versa.
274
+ const horizontalFirst = fromDir
275
+ ? Math.abs(fromDir.x) > Math.abs(fromDir.y)
276
+ : Math.abs(dx) >= Math.abs(dy);
277
+ if (Math.abs(dx) > 0.5 && Math.abs(dy) > 0.5) {
278
+ const bend = horizontalFirst
279
+ ? { x: end.x, y: cursorFrom.y }
280
+ : { x: cursorFrom.x, y: end.y };
281
+ path.push(bend);
282
+ }
283
+ if (toStubPoint)
284
+ path.push(toStubPoint);
285
+ path.push(to);
286
+ return path;
287
+ }
288
+ // Free-floating fallback — single elbow midway. Picks the larger
289
+ // axis so an edge that's mostly horizontal goes horizontal-then-
290
+ // vertical, and vice-versa.
291
+ const dx = to.x - from.x;
292
+ const dy = to.y - from.y;
293
+ const horizontalFirst = Math.abs(dx) >= Math.abs(dy);
294
+ const mid = horizontalFirst ? { x: to.x, y: from.y } : { x: from.x, y: to.y };
295
+ return [from, mid, to];
296
+ };
297
+ /**
298
+ * Outward direction (unit vector) for an edge endpoint anchored to
299
+ * a named side. Returns null when the endpoint has no clear
300
+ * directional hint (centre / corner / free point / non-anchor).
301
+ *
302
+ * Used by the orthogonal routing to add a side-aware stub before
303
+ * bending — matches the look of standard connectors.
304
+ */
305
+ const exitDirectionFor = (endpoint) => {
306
+ if (endpoint.kind !== "anchor")
307
+ return null;
308
+ if (endpoint.anchor.kind !== "named")
309
+ return null;
310
+ switch (endpoint.anchor.name) {
311
+ case "top":
312
+ return { x: 0, y: -1 };
313
+ case "right":
314
+ return { x: 1, y: 0 };
315
+ case "bottom":
316
+ return { x: 0, y: 1 };
317
+ case "left":
318
+ return { x: -1, y: 0 };
319
+ default:
320
+ // top-left / top-right / bottom-left / bottom-right / center / custom
321
+ return null;
322
+ }
323
+ };
324
+ /**
325
+ * Distance from a point to a finite line segment in world coordinates.
326
+ * Used by `findLinkAt` for hit-testing.
327
+ */
328
+ const distanceToSegment = (point, a, b) => {
329
+ const dx = b.x - a.x;
330
+ const dy = b.y - a.y;
331
+ const lenSq = dx * dx + dy * dy;
332
+ if (lenSq === 0) {
333
+ const ex = point.x - a.x;
334
+ const ey = point.y - a.y;
335
+ return Math.hypot(ex, ey);
336
+ }
337
+ let t = ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq;
338
+ t = Math.max(0, Math.min(1, t));
339
+ const cx = a.x + dx * t;
340
+ const cy = a.y + dy * t;
341
+ return Math.hypot(point.x - cx, point.y - cy);
342
+ };
343
+ /**
344
+ * Unit direction a curved link should leave/enter an endpoint along: the
345
+ * edge's outward normal for a named-side anchor, else a fallback along the
346
+ * dominant axis toward `toward` (so a free point / corner / floating end
347
+ * gives a sensible flat-ish curve instead of a fixed one-sided bow).
348
+ */
349
+ const curveEndDir = (endpoint, at, toward) => {
350
+ const named = exitDirectionFor(endpoint);
351
+ if (named)
352
+ return named;
353
+ const dx = toward.x - at.x;
354
+ const dy = toward.y - at.y;
355
+ if (Math.abs(dx) >= Math.abs(dy))
356
+ return { x: Math.sign(dx) || 1, y: 0 };
357
+ return { x: 0, y: Math.sign(dy) || 1 };
358
+ };
359
+ /**
360
+ * Cubic-bezier representation of a curved (bezier) edge: the start point and
361
+ * the list of cubic segments. A no-waypoint span is a single cubic whose
362
+ * control arms follow the endpoints' edge normals (exits/enters perpendicular
363
+ * to the element edge — flowchart look); a waypointed span is a Catmull-Rom
364
+ * spline through the bends. Returns `null` for non-bezier / unresolvable.
365
+ */
366
+ export const getLinkCurveSegments = (scene, edge) => {
367
+ if ((edge.routing ?? "straight") !== "bezier")
368
+ return null;
369
+ // Self-loop → an arc that bows outside the element (see getSelfLoopSpec).
370
+ const loop = getSelfLoopSpec(scene, edge);
371
+ if (loop)
372
+ return selfLoopCurve(loop);
373
+ const path = getLinkPath(scene, edge);
374
+ if (!path || path.length < 2)
375
+ return null;
376
+ const start = req(path[0]);
377
+ const end = req(path[path.length - 1]);
378
+ if (path.length === 2) {
379
+ const dirFrom = curveEndDir(edge.from, start, end);
380
+ const dirTo = curveEndDir(edge.to, end, start);
381
+ return { start, segments: [cubicWithEndTangents(start, end, dirFrom, dirTo)] };
382
+ }
383
+ return { start, segments: catmullRomBeziers(path) };
384
+ };
385
+ /**
386
+ * Hit-test / bounds polyline for an edge: the routed path, except a curved
387
+ * (bezier) edge is flattened to follow the drawn arc (so clicking the
388
+ * visible curve — which bows away from the straight chord — actually hits).
389
+ * Returns `null` when the path is unresolvable.
390
+ */
391
+ export const getLinkCurvePoints = (scene, edge) => {
392
+ if ((edge.routing ?? "straight") !== "bezier")
393
+ return getLinkPath(scene, edge);
394
+ const curve = getLinkCurveSegments(scene, edge);
395
+ if (!curve)
396
+ return null;
397
+ return flattenSegments(curve.start, curve.segments);
398
+ };
399
+ /**
400
+ * Per-span "add a waypoint" handle positions for a selected link's bend
401
+ * editing — one handle per logical span of `[from, ...waypoints, to]`, index i
402
+ * being the span between point i and i+1 (what `beginWaypointDrag(i, insert)`
403
+ * expects). The handle sits at the VISUAL middle of its span:
404
+ * - bezier — the curve point at `t=0.5` of that span's cubic, so the handle
405
+ * lands ON the drawn arc (not the straight chord beside it);
406
+ * - straight — the chord midpoint (the span IS straight).
407
+ * Returns `null` for orthogonal (elbow) links — their segment handles are
408
+ * computed from the routed path by the caller — or when unresolvable.
409
+ */
410
+ export const getLinkWaypointMidpoints = (scene, edge) => {
411
+ const routing = edge.routing ?? "straight";
412
+ if (routing === "orthogonal")
413
+ return null;
414
+ if (routing === "bezier") {
415
+ const curve = getLinkCurveSegments(scene, edge);
416
+ if (!curve)
417
+ return null;
418
+ const mids = [];
419
+ let prev = curve.start;
420
+ for (const s of curve.segments) {
421
+ mids.push(cubicAt(prev, s.c1, s.c2, s.to, 0.5));
422
+ prev = s.to;
423
+ }
424
+ return mids;
425
+ }
426
+ const path = getLinkPath(scene, edge);
427
+ if (!path || path.length < 2)
428
+ return null;
429
+ const mids = [];
430
+ for (let i = 0; i < path.length - 1; i++) {
431
+ const a = req(path[i]);
432
+ const b = req(path[i + 1]);
433
+ mids.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
434
+ }
435
+ return mids;
436
+ };
437
+ /**
438
+ * Topmost edge under `worldPoint`, or `null` if none is within
439
+ * `threshold` world-pixels of any segment. Iterates edges in layer
440
+ * order; later layers (rendered on top) win when paths overlap.
441
+ *
442
+ * Curved (bezier) edges are tested against the flattened curve (via
443
+ * `getLinkCurvePoints`) so the clickable line matches the drawn arc.
444
+ */
445
+ export const findLinkAt = (scene, worldPoint, threshold = 5) => {
446
+ let best = null;
447
+ for (const edge of scene.links.values()) {
448
+ const path = getLinkCurvePoints(scene, edge);
449
+ if (!path)
450
+ continue;
451
+ let minDistance = Infinity;
452
+ for (let i = 1; i < path.length; i++) {
453
+ const d = distanceToSegment(worldPoint, req(path[i - 1]), req(path[i]));
454
+ if (d < minDistance)
455
+ minDistance = d;
456
+ }
457
+ if (minDistance > threshold)
458
+ continue;
459
+ // Layer ordering: a later edge in iteration order is rendered on top,
460
+ // so it should win for ties.
461
+ if (!best || minDistance <= best.distance) {
462
+ best = { edge, distance: minDistance };
463
+ }
464
+ }
465
+ return best?.edge ?? null;
466
+ };
467
+ //# sourceMappingURL=edge-geometry.js.map