@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/a11y.d.ts +23 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.js.map +1 -0
- package/dist/anchors.d.ts +120 -0
- package/dist/anchors.d.ts.map +1 -0
- package/dist/anchors.js +322 -0
- package/dist/anchors.js.map +1 -0
- package/dist/annotation-geometry.d.ts +11 -0
- package/dist/annotation-geometry.d.ts.map +1 -0
- package/dist/annotation-geometry.js +19 -0
- package/dist/annotation-geometry.js.map +1 -0
- package/dist/annotation.d.ts +32 -0
- package/dist/annotation.d.ts.map +1 -0
- package/dist/annotation.js +2 -0
- package/dist/annotation.js.map +1 -0
- package/dist/constants.d.ts +186 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +186 -0
- package/dist/constants.js.map +1 -0
- package/dist/container.d.ts +112 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +227 -0
- package/dist/container.js.map +1 -0
- package/dist/diff.d.ts +22 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +18 -0
- package/dist/diff.js.map +1 -0
- package/dist/edge-curve.d.ts +37 -0
- package/dist/edge-curve.d.ts.map +1 -0
- package/dist/edge-curve.js +72 -0
- package/dist/edge-curve.js.map +1 -0
- package/dist/edge-geometry.d.ts +96 -0
- package/dist/edge-geometry.d.ts.map +1 -0
- package/dist/edge-geometry.js +467 -0
- package/dist/edge-geometry.js.map +1 -0
- package/dist/edge.d.ts +206 -0
- package/dist/edge.d.ts.map +1 -0
- package/dist/edge.js +2 -0
- package/dist/edge.js.map +1 -0
- package/dist/elbow-link.d.ts +23 -0
- package/dist/elbow-link.d.ts.map +1 -0
- package/dist/elbow-link.js +419 -0
- package/dist/elbow-link.js.map +1 -0
- package/dist/elbow-router.d.ts +20 -0
- package/dist/elbow-router.d.ts.map +1 -0
- package/dist/elbow-router.js +218 -0
- package/dist/elbow-router.js.map +1 -0
- package/dist/file.d.ts +41 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/file.js +13 -0
- package/dist/file.js.map +1 -0
- package/dist/heading.d.ts +42 -0
- package/dist/heading.d.ts.map +1 -0
- package/dist/heading.js +77 -0
- package/dist/heading.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/layer.d.ts +18 -0
- package/dist/layer.d.ts.map +1 -0
- package/dist/layer.js +2 -0
- package/dist/layer.js.map +1 -0
- package/dist/layout-registry.d.ts +35 -0
- package/dist/layout-registry.d.ts.map +1 -0
- package/dist/layout-registry.js +13 -0
- package/dist/layout-registry.js.map +1 -0
- package/dist/layout.d.ts +128 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +453 -0
- package/dist/layout.js.map +1 -0
- package/dist/operations.d.ts +32 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +105 -0
- package/dist/operations.js.map +1 -0
- package/dist/outline.d.ts +36 -0
- package/dist/outline.d.ts.map +1 -0
- package/dist/outline.js +129 -0
- package/dist/outline.js.map +1 -0
- package/dist/patch.d.ts +57 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +47 -0
- package/dist/patch.js.map +1 -0
- package/dist/queries.d.ts +115 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +403 -0
- package/dist/queries.js.map +1 -0
- package/dist/render-bounds.d.ts +36 -0
- package/dist/render-bounds.d.ts.map +1 -0
- package/dist/render-bounds.js +36 -0
- package/dist/render-bounds.js.map +1 -0
- package/dist/scene.d.ts +80 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +129 -0
- package/dist/scene.js.map +1 -0
- package/dist/shape.d.ts +306 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +192 -0
- package/dist/shape.js.map +1 -0
- package/dist/snap.d.ts +88 -0
- package/dist/snap.d.ts.map +1 -0
- package/dist/snap.js +128 -0
- package/dist/snap.js.map +1 -0
- package/dist/spatial.d.ts +38 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/spatial.js +95 -0
- package/dist/spatial.js.map +1 -0
- package/dist/style.d.ts +87 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +43 -0
- package/dist/style.js.map +1 -0
- package/dist/text-measure.d.ts +29 -0
- package/dist/text-measure.d.ts.map +1 -0
- package/dist/text-measure.js +8 -0
- package/dist/text-measure.js.map +1 -0
- package/dist/three-way-merge.d.ts +59 -0
- package/dist/three-way-merge.d.ts.map +1 -0
- package/dist/three-way-merge.js +108 -0
- package/dist/three-way-merge.js.map +1 -0
- package/dist/viewport.d.ts +78 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +62 -0
- package/dist/viewport.js.map +1 -0
- 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
|