@statelyai/layout 0.4.2 → 0.5.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/README.md +40 -3
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +1 -1
- package/dist/elkjs/index.d.mts +1 -1
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/{elkjs-CSi7oB5J.mjs → elkjs-DBkalvt1.mjs} +25 -3
- package/dist/{elkjs-CnBvdyBu.cjs → elkjs-DonMqGjf.cjs} +25 -3
- package/dist/{index-B6cWNrMw.d.cts → index-0YVSvMvA.d.mts} +13 -3
- package/dist/{index-CJQLUKD9.d.mts → index-C0J4ttZe.d.cts} +13 -3
- package/dist/{index-DT6lPMfS.d.cts → index-Xc4R6y3z.d.cts} +9 -2
- package/dist/{index-F1U-fCTu.d.mts → index-lSp0oCJC.d.mts} +9 -2
- package/dist/index.cjs +7 -4
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-BsAdm_u8.mjs → layered-CmL7oynw.mjs} +245 -69
- package/dist/{layered-BJu8A21s.cjs → layered-SB2mlAqt.cjs} +245 -69
- package/dist/{layout-engine-DnlmGxHL.mjs → layout-engine-BXoIpYL4.mjs} +1 -1
- package/dist/{layout-engine-BFP1TZbF.cjs → layout-engine-CAyJ-I-u.cjs} +1 -1
- package/dist/routing/index.cjs +5 -2
- package/dist/routing/index.d.cts +3 -3
- package/dist/routing/index.d.mts +3 -3
- package/dist/routing/index.mjs +2 -2
- package/dist/routing-D-LfMXOF.mjs +5189 -0
- package/dist/routing-Phxu1R0x.cjs +5423 -0
- package/dist/{types-CT2akdK0.d.mts → types-D8pYSn8I.d.cts} +112 -2
- package/dist/{types-uxPlGRof.d.cts → types-MAUtJfKQ.d.mts} +112 -2
- package/package.json +4 -1
- package/src/authoring/partial.ts +1 -7
- package/src/authoring/routing.ts +64 -19
- package/src/elkjs/index.ts +26 -0
- package/src/layered/feedback-chains.ts +22 -4
- package/src/layered/index.ts +173 -41
- package/src/layered/north-south-ports.ts +17 -4
- package/src/layered/strategies.ts +143 -15
- package/src/routing/algorithms.ts +1436 -148
- package/src/routing/bezier-fit.ts +605 -0
- package/src/routing/bezier-visibility.ts +107 -0
- package/src/routing/bezier.ts +596 -0
- package/src/routing/engine.ts +65 -13
- package/src/routing/index.ts +14 -0
- package/src/routing/model.ts +185 -1
- package/src/routing/path.ts +1 -1
- package/src/routing/policy-geometry.ts +536 -0
- package/src/routing/search.ts +10 -2
- package/src/routing/types.ts +77 -1
- package/dist/routing-CR8flv8b.mjs +0 -2780
- package/dist/routing-QIdGGFBe.cjs +0 -2996
package/src/routing/engine.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GraphDiff } from "@statelyai/graph";
|
|
2
|
+
import { expandLabelFamilies, labelFamily, routeBezierEdge } from "./bezier";
|
|
2
3
|
import { conflictCost, reservations, reservationBounds, outsideTerminal } from "./coordination";
|
|
3
4
|
import { segmentBounds } from "./search";
|
|
4
5
|
import { routeBounds, routeEdge, type Metrics, type Batch } from "./algorithms";
|
|
@@ -6,7 +7,8 @@ import {
|
|
|
6
7
|
edgeGeometry,
|
|
7
8
|
freeze,
|
|
8
9
|
groupKey,
|
|
9
|
-
|
|
10
|
+
obstacleLabelRect,
|
|
11
|
+
effectivePolicy,
|
|
10
12
|
members,
|
|
11
13
|
nodeGeometry,
|
|
12
14
|
settings,
|
|
@@ -85,7 +87,7 @@ function initialState(graph: RoutingGraph, config: RoutingSettings, style: Route
|
|
|
85
87
|
state = { ...state, obstacles: state.obstacles.set(`n:${id}`, worldRect(n, state)) };
|
|
86
88
|
for (const [id, e] of edges) {
|
|
87
89
|
state = edgeMembership(state, e, style, true);
|
|
88
|
-
state = { ...state, obstacles: state.obstacles.set(`e:${id}`,
|
|
90
|
+
state = { ...state, obstacles: state.obstacles.set(`e:${id}`, obstacleLabelRect(e, state)) };
|
|
89
91
|
}
|
|
90
92
|
return state;
|
|
91
93
|
}
|
|
@@ -111,12 +113,30 @@ function calculate(
|
|
|
111
113
|
plans: new Map(),
|
|
112
114
|
};
|
|
113
115
|
const pending = new Set(affected);
|
|
116
|
+
expandLabelFamilies(pending, input);
|
|
117
|
+
if (previous) expandLabelFamilies(pending, previous);
|
|
114
118
|
const processed = new Set<string>();
|
|
119
|
+
const familyDepth = new Map<string, number>();
|
|
120
|
+
const familyRoots = new Map<string, string>();
|
|
121
|
+
for (const [id, config] of Object.entries(state.settings.edges)) {
|
|
122
|
+
if (config.sourceLabel === undefined) continue;
|
|
123
|
+
const family = labelFamily(id, input);
|
|
124
|
+
for (let index = 0; index < family.length; index++) {
|
|
125
|
+
const member = family[index]!;
|
|
126
|
+
familyRoots.set(member, family.at(-1)!);
|
|
127
|
+
familyDepth.set(member, family.length - index);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
115
130
|
const priorities = new Map<string, string>();
|
|
116
131
|
const priority = (id: string) => {
|
|
117
132
|
let result = priorities.get(id);
|
|
118
133
|
if (result !== undefined) return result;
|
|
119
134
|
const edge = state.edges.get(id) ?? previous?.edges.get(id);
|
|
135
|
+
const familyRoot = familyRoots.get(id);
|
|
136
|
+
if (familyRoot !== undefined) {
|
|
137
|
+
priorities.set(id, familyRoot);
|
|
138
|
+
return familyRoot;
|
|
139
|
+
}
|
|
120
140
|
const key = edge && groupKey(edge, style, state.settings);
|
|
121
141
|
result = key
|
|
122
142
|
? ((state.edges.has(id) ? state.groups : previous?.groups)?.get(key)?.keys().next().value ??
|
|
@@ -125,12 +145,17 @@ function calculate(
|
|
|
125
145
|
priorities.set(id, result);
|
|
126
146
|
return result;
|
|
127
147
|
};
|
|
128
|
-
const
|
|
129
|
-
priority(
|
|
148
|
+
const compare = (a: string, b: string) =>
|
|
149
|
+
(priority(a) < priority(b) ? -1 : priority(a) > priority(b) ? 1 : 0) ||
|
|
150
|
+
(familyDepth.get(a) ?? 1) - (familyDepth.get(b) ?? 1) ||
|
|
151
|
+
(a < b ? -1 : a > b ? 1 : 0);
|
|
152
|
+
const later = (candidate: string, id: string) => compare(candidate, id) > 0;
|
|
130
153
|
const invalidate = (id: string, bounds: ReturnType<typeof routeBounds>) => {
|
|
131
154
|
for (const candidate of routeIndex.query(inflate(bounds, state.settings.edgeSpacing))) {
|
|
132
155
|
if (!later(candidate, id) || processed.has(candidate)) continue;
|
|
133
156
|
pending.add(candidate);
|
|
157
|
+
expandLabelFamilies(pending, input);
|
|
158
|
+
for (const done of processed) pending.delete(done);
|
|
134
159
|
const edge = state.edges.get(candidate);
|
|
135
160
|
const key = edge && groupKey(edge, style, state.settings);
|
|
136
161
|
if (key)
|
|
@@ -160,11 +185,7 @@ function calculate(
|
|
|
160
185
|
}
|
|
161
186
|
const segmentCache = new Map<string, ReturnType<typeof reservations>>();
|
|
162
187
|
while (pending.size) {
|
|
163
|
-
const id = [...pending].sort(
|
|
164
|
-
(a, b) =>
|
|
165
|
-
(priority(a) < priority(b) ? -1 : priority(a) > priority(b) ? 1 : 0) ||
|
|
166
|
-
(a < b ? -1 : a > b ? 1 : 0),
|
|
167
|
-
)[0]!;
|
|
188
|
+
const id = [...pending].sort(compare)[0]!;
|
|
168
189
|
pending.delete(id);
|
|
169
190
|
processed.add(id);
|
|
170
191
|
const edge = state.edges.get(id),
|
|
@@ -200,7 +221,14 @@ function calculate(
|
|
|
200
221
|
const node = state.nodes.get(id),
|
|
201
222
|
bounds = node && worldRect(node, state);
|
|
202
223
|
return bounds
|
|
203
|
-
? [
|
|
224
|
+
? [
|
|
225
|
+
inflate(
|
|
226
|
+
bounds,
|
|
227
|
+
state.settings.clearance +
|
|
228
|
+
(style === "organic" ? 0 : state.settings.edgeSpacing * 2) +
|
|
229
|
+
1,
|
|
230
|
+
),
|
|
231
|
+
]
|
|
204
232
|
: [];
|
|
205
233
|
});
|
|
206
234
|
let segments = segmentCache.get(otherId);
|
|
@@ -224,9 +252,11 @@ function calculate(
|
|
|
224
252
|
}
|
|
225
253
|
return result;
|
|
226
254
|
};
|
|
255
|
+
batch.policy = effectivePolicy(state.settings, id);
|
|
256
|
+
const costSettings = { ...state.settings, ...batch.policy?.collisions };
|
|
227
257
|
batch.edgeCost = (a, b) =>
|
|
228
258
|
query(segmentBounds(a, b)).reduce(
|
|
229
|
-
(cost, other) => cost + conflictCost(a, b, other,
|
|
259
|
+
(cost, other) => cost + conflictCost(a, b, other, costSettings),
|
|
230
260
|
0,
|
|
231
261
|
);
|
|
232
262
|
batch.conflicts = (a, b) =>
|
|
@@ -237,7 +267,27 @@ function calculate(
|
|
|
237
267
|
);
|
|
238
268
|
batch.guides = (bounds) =>
|
|
239
269
|
query(bounds).map((segment) => reservationBounds(segment, state.settings.edgeSpacing));
|
|
240
|
-
|
|
270
|
+
batch.labelOverlaps = (bounds) => {
|
|
271
|
+
batch.dependencyBounds = batch.dependencyBounds
|
|
272
|
+
? union(batch.dependencyBounds, bounds)
|
|
273
|
+
: bounds;
|
|
274
|
+
return routeIndex
|
|
275
|
+
.query(bounds)
|
|
276
|
+
.filter((otherId) => {
|
|
277
|
+
const label = routes.get(otherId)?.label;
|
|
278
|
+
return later(id, otherId) && label && intersects(bounds, label.bounds);
|
|
279
|
+
})
|
|
280
|
+
.map((otherId) => `e:${otherId}`);
|
|
281
|
+
};
|
|
282
|
+
batch.routes = routes;
|
|
283
|
+
batch.previous = oldRoute;
|
|
284
|
+
batch.previousSettings = previous?.settings;
|
|
285
|
+
let route =
|
|
286
|
+
style === "bezier" &&
|
|
287
|
+
!batch.policy &&
|
|
288
|
+
!routes.get(state.settings.edges[id]?.sourceLabel ?? "")?.label
|
|
289
|
+
? routeBezierEdge(edge, input, routes, counts, batch)
|
|
290
|
+
: routeEdge(edge, input, style, counts, batch);
|
|
241
291
|
if (oldRoute && JSON.stringify(oldRoute) === JSON.stringify(route)) route = oldRoute;
|
|
242
292
|
const drawnBounds = routeBounds(route);
|
|
243
293
|
if (route !== oldRoute) {
|
|
@@ -390,7 +440,7 @@ function updateState(
|
|
|
390
440
|
}
|
|
391
441
|
for (const id of changedEdges) {
|
|
392
442
|
const edge = state.edges.get(id);
|
|
393
|
-
invalidateObstacle(`e:${id}`, edge &&
|
|
443
|
+
invalidateObstacle(`e:${id}`, edge && obstacleLabelRect(edge, state));
|
|
394
444
|
affected.add(id);
|
|
395
445
|
}
|
|
396
446
|
// Terminal placement depends on current incident edges, including neighbors
|
|
@@ -426,6 +476,8 @@ function updateState(
|
|
|
426
476
|
if (key !== undefined)
|
|
427
477
|
for (const peer of version.groups.get(key)?.keys() ?? []) affected.add(peer);
|
|
428
478
|
}
|
|
479
|
+
expandLabelFamilies(affected, prior);
|
|
480
|
+
expandLabelFamilies(affected, state);
|
|
429
481
|
return { state, affected };
|
|
430
482
|
}
|
|
431
483
|
export function createRoutingStrategy(id: RouteStyle): NativeRoutingStrategy {
|
package/src/routing/index.ts
CHANGED
|
@@ -34,9 +34,14 @@ export {
|
|
|
34
34
|
getTangentAtLength,
|
|
35
35
|
pathFromPoints,
|
|
36
36
|
roundCorners,
|
|
37
|
+
segmentCrossesRect,
|
|
37
38
|
toSvgPath,
|
|
38
39
|
} from "./path";
|
|
39
40
|
export type {
|
|
41
|
+
BezierEndpoint,
|
|
42
|
+
BezierGuide,
|
|
43
|
+
BezierRoutingOptions,
|
|
44
|
+
BezierEdgeSettings,
|
|
40
45
|
RouteAttachment,
|
|
41
46
|
EdgeRoutingSettings,
|
|
42
47
|
NativeRoutingStrategy,
|
|
@@ -55,6 +60,11 @@ export type {
|
|
|
55
60
|
RoutingGraph,
|
|
56
61
|
RoutingMetrics,
|
|
57
62
|
RoutingSettings,
|
|
63
|
+
RoutingPolicy,
|
|
64
|
+
RoutingCurve,
|
|
65
|
+
RoutingTangent,
|
|
66
|
+
RoutingLabel,
|
|
67
|
+
RouteLabelPlacement,
|
|
58
68
|
RoutingSnapshot,
|
|
59
69
|
RoutingStrategy,
|
|
60
70
|
RoutingUpdate,
|
|
@@ -67,3 +77,7 @@ export {
|
|
|
67
77
|
routeToPolylines,
|
|
68
78
|
routeToPoints,
|
|
69
79
|
} from "./adapters";
|
|
80
|
+
|
|
81
|
+
export { trimBezierPathStart } from "./bezier-fit";
|
|
82
|
+
|
|
83
|
+
export { routingPolicies } from "./model";
|
package/src/routing/model.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { GraphEdge } from "@statelyai/graph";
|
|
2
2
|
import { PersistentMap } from "./persistent";
|
|
3
3
|
import { SpatialIndex } from "./spatial";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
Route,
|
|
6
|
+
RouteBounds,
|
|
7
|
+
RoutePoint,
|
|
8
|
+
RoutingSettings,
|
|
9
|
+
RouteSide,
|
|
10
|
+
RoutingPolicy,
|
|
11
|
+
RoutingTangent,
|
|
12
|
+
} from "./types";
|
|
5
13
|
export interface NodeGeometry {
|
|
6
14
|
readonly id: string;
|
|
7
15
|
readonly parentId?: string | null;
|
|
@@ -29,6 +37,8 @@ export interface EdgeGeometry {
|
|
|
29
37
|
readonly height?: number;
|
|
30
38
|
}
|
|
31
39
|
export interface Settings {
|
|
40
|
+
readonly policy?: RoutingPolicy;
|
|
41
|
+
readonly bezier: RoutingSettings["bezier"];
|
|
32
42
|
readonly coordinateSpace: "parent" | "world";
|
|
33
43
|
readonly clearance: number;
|
|
34
44
|
readonly radius: number;
|
|
@@ -42,6 +52,8 @@ export interface Settings {
|
|
|
42
52
|
}
|
|
43
53
|
export function settings(input: RoutingSettings = {}): Settings {
|
|
44
54
|
const result = {
|
|
55
|
+
policy: normalizePolicy(input.policy),
|
|
56
|
+
bezier: structuredClone(input.bezier),
|
|
45
57
|
coordinateSpace: input.coordinateSpace ?? "parent",
|
|
46
58
|
clearance: input.clearance ?? 8,
|
|
47
59
|
radius: input.radius ?? 10,
|
|
@@ -62,6 +74,27 @@ export function settings(input: RoutingSettings = {}): Settings {
|
|
|
62
74
|
!Number.isInteger(result.organicIterations)
|
|
63
75
|
)
|
|
64
76
|
throw new RangeError("Search and iteration budgets must be integers");
|
|
77
|
+
for (const config of [result.bezier, ...Object.values(result.edges).map((edge) => edge.bezier)]) {
|
|
78
|
+
if (
|
|
79
|
+
config?.targetClearance !== undefined &&
|
|
80
|
+
(!Number.isFinite(config.targetClearance) || config.targetClearance < 0)
|
|
81
|
+
)
|
|
82
|
+
throw new RangeError("Bézier targetClearance must be finite and non-negative");
|
|
83
|
+
}
|
|
84
|
+
for (const edge of Object.values(result.edges)) {
|
|
85
|
+
if (edge.policy) Object.assign(edge, { policy: normalizePolicy(edge.policy) });
|
|
86
|
+
for (const point of edge.waypoints ?? [])
|
|
87
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y))
|
|
88
|
+
throw new RangeError("Waypoints must be finite");
|
|
89
|
+
const tangent = edge.bezier?.labelTangent;
|
|
90
|
+
if (
|
|
91
|
+
tangent &&
|
|
92
|
+
(!Number.isFinite(tangent.x) ||
|
|
93
|
+
!Number.isFinite(tangent.y) ||
|
|
94
|
+
!Math.hypot(tangent.x, tangent.y))
|
|
95
|
+
)
|
|
96
|
+
throw new RangeError("Bézier labelTangent must be finite and nonzero");
|
|
97
|
+
}
|
|
65
98
|
return freeze(result);
|
|
66
99
|
}
|
|
67
100
|
export function freeze<T>(value: T): T {
|
|
@@ -216,3 +249,154 @@ export function groupKey(edge: EdgeGeometry, style: string, config: Settings): s
|
|
|
216
249
|
: `source:${JSON.stringify([edge.sourceId, edge.sourcePort ?? null])}`;
|
|
217
250
|
return `pair:${JSON.stringify([edge.sourceId, edge.targetId].sort())}`;
|
|
218
251
|
}
|
|
252
|
+
|
|
253
|
+
function normalizePolicy(input: RoutingPolicy | undefined): RoutingPolicy | undefined {
|
|
254
|
+
if (!input) return;
|
|
255
|
+
const value = structuredClone(input);
|
|
256
|
+
const oneOf = (value: unknown, allowed: readonly string[], name: string) => {
|
|
257
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
258
|
+
throw new RangeError(`Invalid ${name}`);
|
|
259
|
+
};
|
|
260
|
+
if (value.curve)
|
|
261
|
+
oneOf(
|
|
262
|
+
value.curve.kind,
|
|
263
|
+
["polyline", "bezier", "catmull-rom", "rounded-orthogonal"],
|
|
264
|
+
"curve kind",
|
|
265
|
+
);
|
|
266
|
+
if (value.label) oneOf(value.label.kind, ["follow", "fixed", "through", "gap"], "label kind");
|
|
267
|
+
if (value.monotonic) {
|
|
268
|
+
oneOf(value.monotonic.axis, ["x", "y", "both"], "monotonic axis");
|
|
269
|
+
oneOf(value.monotonic.strength, ["prefer", "require"], "monotonic strength");
|
|
270
|
+
}
|
|
271
|
+
if (value.stability)
|
|
272
|
+
oneOf(value.stability.kind, ["recompute", "preserve-valid"], "stability kind");
|
|
273
|
+
for (const name of ["nodes", "labels"] as const)
|
|
274
|
+
if (value.collisions?.[name] !== undefined)
|
|
275
|
+
oneOf(value.collisions[name], ["avoid", "allow"], `${name} collision mode`);
|
|
276
|
+
|
|
277
|
+
const nonnegative = (n: number | undefined, name: string) => {
|
|
278
|
+
if (n !== undefined && (!Number.isFinite(n) || n < 0))
|
|
279
|
+
throw new RangeError(`${name} must be finite and non-negative`);
|
|
280
|
+
};
|
|
281
|
+
function tangent(t: RoutingTangent | undefined): RoutingTangent | undefined {
|
|
282
|
+
if (t) oneOf(t.kind, ["automatic", "border-normal", "vector"], "tangent kind");
|
|
283
|
+
if (t?.kind !== "vector") return t;
|
|
284
|
+
const { x, y } = t.direction,
|
|
285
|
+
length = Math.hypot(x, y);
|
|
286
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(length) || !length)
|
|
287
|
+
throw new RangeError("Tangent direction must be finite and nonzero");
|
|
288
|
+
return { kind: "vector", direction: { x: x / length, y: y / length } };
|
|
289
|
+
}
|
|
290
|
+
if (value.curve?.kind === "bezier") nonnegative(value.curve.curvature, "curvature");
|
|
291
|
+
if (value.curve?.kind === "rounded-orthogonal") nonnegative(value.curve.radius, "radius");
|
|
292
|
+
nonnegative(value.collisions?.crossingPenalty, "crossingPenalty");
|
|
293
|
+
nonnegative(value.collisions?.overlapPenalty, "overlapPenalty");
|
|
294
|
+
if (value.label?.kind === "follow") {
|
|
295
|
+
const { fraction = 0.5, offset = 0 } = value.label;
|
|
296
|
+
if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1)
|
|
297
|
+
throw new RangeError("Label fraction must be in [0, 1]");
|
|
298
|
+
if (!Number.isFinite(offset)) throw new RangeError("Label offset must be finite");
|
|
299
|
+
}
|
|
300
|
+
return freeze({
|
|
301
|
+
...value,
|
|
302
|
+
...(value.sourceTangent ? { sourceTangent: tangent(value.sourceTangent) } : {}),
|
|
303
|
+
...(value.targetTangent ? { targetTangent: tangent(value.targetTangent) } : {}),
|
|
304
|
+
...(value.label?.kind === "through" || value.label?.kind === "gap"
|
|
305
|
+
? {
|
|
306
|
+
label: {
|
|
307
|
+
...value.label,
|
|
308
|
+
...(value.label.tangent ? { tangent: tangent(value.label.tangent) } : {}),
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
: {}),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
export function effectivePolicy(config: Settings, edgeId: string): RoutingPolicy | undefined {
|
|
315
|
+
const edge = config.edges[edgeId],
|
|
316
|
+
local = edge?.policy,
|
|
317
|
+
global = config.policy;
|
|
318
|
+
if (!local && !global) return;
|
|
319
|
+
const legacyAttachment = edge?.bezier?.attachmentMode;
|
|
320
|
+
const legacyTangent: RoutingTangent | undefined =
|
|
321
|
+
legacyAttachment === "orthogonal"
|
|
322
|
+
? { kind: "border-normal" }
|
|
323
|
+
: legacyAttachment === "floating"
|
|
324
|
+
? { kind: "automatic" }
|
|
325
|
+
: undefined;
|
|
326
|
+
let label = local?.label ?? global?.label;
|
|
327
|
+
const legacyLabel = edge?.bezier?.labelTangent;
|
|
328
|
+
if (!local?.label && legacyLabel && (label?.kind === "through" || label?.kind === "gap")) {
|
|
329
|
+
const length = Math.hypot(legacyLabel.x, legacyLabel.y);
|
|
330
|
+
label = {
|
|
331
|
+
...label,
|
|
332
|
+
tangent: {
|
|
333
|
+
kind: "vector",
|
|
334
|
+
direction: { x: legacyLabel.x / length, y: legacyLabel.y / length },
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
curve: local?.curve ?? global?.curve,
|
|
340
|
+
sourceTangent: local?.sourceTangent ?? legacyTangent ?? global?.sourceTangent,
|
|
341
|
+
targetTangent: local?.targetTangent ?? legacyTangent ?? global?.targetTangent,
|
|
342
|
+
label,
|
|
343
|
+
collisions: {
|
|
344
|
+
nodes: local?.collisions?.nodes ?? global?.collisions?.nodes ?? "avoid",
|
|
345
|
+
labels: local?.collisions?.labels ?? global?.collisions?.labels ?? "avoid",
|
|
346
|
+
crossingPenalty:
|
|
347
|
+
local?.collisions?.crossingPenalty ??
|
|
348
|
+
global?.collisions?.crossingPenalty ??
|
|
349
|
+
config.crossingPenalty,
|
|
350
|
+
overlapPenalty:
|
|
351
|
+
local?.collisions?.overlapPenalty ??
|
|
352
|
+
global?.collisions?.overlapPenalty ??
|
|
353
|
+
config.overlapPenalty,
|
|
354
|
+
},
|
|
355
|
+
monotonic: local?.monotonic ?? global?.monotonic,
|
|
356
|
+
stability: local?.stability ?? global?.stability,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export function obstacleLabelRect(
|
|
360
|
+
edge: EdgeGeometry,
|
|
361
|
+
state: Pick<State, "nodes" | "settings">,
|
|
362
|
+
): RouteBounds | undefined {
|
|
363
|
+
return effectivePolicy(state.settings, edge.id)?.label?.kind === "follow"
|
|
364
|
+
? undefined
|
|
365
|
+
: labelRect(edge, state);
|
|
366
|
+
}
|
|
367
|
+
const presetCollisions = {
|
|
368
|
+
nodes: "avoid",
|
|
369
|
+
labels: "avoid",
|
|
370
|
+
crossingPenalty: 80,
|
|
371
|
+
overlapPenalty: 8,
|
|
372
|
+
} as const;
|
|
373
|
+
export const routingPolicies: Readonly<{
|
|
374
|
+
smooth: RoutingPolicy;
|
|
375
|
+
diagram: RoutingPolicy;
|
|
376
|
+
authored: RoutingPolicy;
|
|
377
|
+
}> = freeze({
|
|
378
|
+
smooth: {
|
|
379
|
+
curve: { kind: "bezier", curvature: 0.5 },
|
|
380
|
+
sourceTangent: { kind: "automatic" },
|
|
381
|
+
targetTangent: { kind: "automatic" },
|
|
382
|
+
label: { kind: "follow", fraction: 0.5 },
|
|
383
|
+
collisions: presetCollisions,
|
|
384
|
+
stability: { kind: "recompute" },
|
|
385
|
+
},
|
|
386
|
+
diagram: {
|
|
387
|
+
curve: { kind: "rounded-orthogonal", radius: 10 },
|
|
388
|
+
sourceTangent: { kind: "border-normal" },
|
|
389
|
+
targetTangent: { kind: "border-normal" },
|
|
390
|
+
label: { kind: "follow", fraction: 0.5 },
|
|
391
|
+
collisions: presetCollisions,
|
|
392
|
+
stability: { kind: "recompute" },
|
|
393
|
+
},
|
|
394
|
+
authored: {
|
|
395
|
+
curve: { kind: "catmull-rom" },
|
|
396
|
+
sourceTangent: { kind: "automatic" },
|
|
397
|
+
targetTangent: { kind: "automatic" },
|
|
398
|
+
label: { kind: "follow", fraction: 0.5 },
|
|
399
|
+
collisions: presetCollisions,
|
|
400
|
+
stability: { kind: "recompute" },
|
|
401
|
+
},
|
|
402
|
+
});
|
package/src/routing/path.ts
CHANGED
|
@@ -22,7 +22,7 @@ function pointLineDistance(p: RoutePoint, a: RoutePoint, b: RoutePoint): number
|
|
|
22
22
|
}
|
|
23
23
|
type Arc = Extract<RouteSegment, { kind: "arc" }>;
|
|
24
24
|
/** SVG endpoint-to-center conversion, including SVG's radius correction. */
|
|
25
|
-
function arcCenter(start: RoutePoint, arc: Arc) {
|
|
25
|
+
export function arcCenter(start: RoutePoint, arc: Arc) {
|
|
26
26
|
let rx = Math.abs(arc.rx),
|
|
27
27
|
ry = Math.abs(arc.ry);
|
|
28
28
|
if (!rx || !ry || distance(start, arc.to) === 0) return undefined;
|