@statelyai/layout 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +4 -2
- package/README.md +221 -13
- package/dist/elkjs/index.cjs +3 -2
- package/dist/elkjs/index.d.cts +13 -2
- package/dist/elkjs/index.d.mts +13 -2
- package/dist/elkjs/index.mjs +3 -3
- package/dist/elkjs/main.cjs +2 -2
- package/dist/elkjs/main.d.cts +2 -2
- package/dist/elkjs/main.d.mts +2 -2
- package/dist/elkjs/main.mjs +2 -2
- package/dist/elkjs/worker-api.cjs +1 -1
- package/dist/elkjs/worker-api.d.cts +2 -2
- package/dist/elkjs/worker-api.d.mts +2 -2
- package/dist/elkjs/worker-api.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/index-B6cWNrMw.d.cts +80 -0
- package/dist/index-CJQLUKD9.d.mts +80 -0
- package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
- package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
- package/dist/index.cjs +39 -9
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -3
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +3 -2
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
- package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
- package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +29 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-CR8flv8b.mjs +2780 -0
- package/dist/routing-QIdGGFBe.cjs +2996 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
- package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
- package/package.json +14 -3
- package/src/authoring/coordinates.ts +38 -10
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1397 -452
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/routes.ts +147 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +8 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +445 -0
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +911 -261
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +34 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +143 -0
- package/src/routing/algorithms.ts +1088 -0
- package/src/routing/coordination.ts +91 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +69 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +358 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
- package/dist/elkjs-CFlfkEBz.mjs +0 -1528
- /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
- /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
- /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
- /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EntityRect, Graph, GraphDiff, Point } from "@statelyai/graph";
|
|
2
|
+
|
|
3
|
+
//#region src/routing/types.d.ts
|
|
4
|
+
type RoutePoint = Readonly<Point>;
|
|
5
|
+
type RouteSegment = {
|
|
6
|
+
readonly kind: "line";
|
|
7
|
+
readonly to: RoutePoint;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "quadratic";
|
|
10
|
+
readonly control: RoutePoint;
|
|
11
|
+
readonly to: RoutePoint;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "cubic";
|
|
14
|
+
readonly control1: RoutePoint;
|
|
15
|
+
readonly control2: RoutePoint;
|
|
16
|
+
readonly to: RoutePoint;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "arc";
|
|
19
|
+
readonly rx: number;
|
|
20
|
+
readonly ry: number;
|
|
21
|
+
readonly rotation: number;
|
|
22
|
+
readonly largeArc: boolean;
|
|
23
|
+
readonly sweep: boolean;
|
|
24
|
+
readonly to: RoutePoint;
|
|
25
|
+
};
|
|
26
|
+
interface RoutePath {
|
|
27
|
+
readonly start: RoutePoint;
|
|
28
|
+
readonly segments: readonly RouteSegment[];
|
|
29
|
+
}
|
|
30
|
+
type RouteEndpoint = {
|
|
31
|
+
readonly kind: "node";
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
readonly port?: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "label";
|
|
36
|
+
readonly edgeId: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "junction";
|
|
39
|
+
readonly id: string;
|
|
40
|
+
};
|
|
41
|
+
interface RouteSection {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly from: RouteEndpoint;
|
|
44
|
+
readonly to: RouteEndpoint;
|
|
45
|
+
readonly path: RoutePath;
|
|
46
|
+
/** Same ID denotes a shared trunk; render it once. */
|
|
47
|
+
readonly sharedId?: string;
|
|
48
|
+
}
|
|
49
|
+
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION";
|
|
50
|
+
interface RoutingDiagnostic {
|
|
51
|
+
readonly code: RoutingDiagnosticCode;
|
|
52
|
+
readonly edgeId: string;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
}
|
|
55
|
+
interface Route {
|
|
56
|
+
readonly edgeId: string;
|
|
57
|
+
readonly sections: readonly RouteSection[];
|
|
58
|
+
readonly status: "routed" | "fallback";
|
|
59
|
+
readonly diagnostics: readonly RoutingDiagnostic[];
|
|
60
|
+
}
|
|
61
|
+
type RoutePatch = {
|
|
62
|
+
readonly op: "set";
|
|
63
|
+
readonly edgeId: string;
|
|
64
|
+
readonly route: Route;
|
|
65
|
+
} | {
|
|
66
|
+
readonly op: "delete";
|
|
67
|
+
readonly edgeId: string;
|
|
68
|
+
};
|
|
69
|
+
type RouteStyle = "straight" | "bezier" | "orthogonal" | "polyline" | "octilinear" | "curved" | "organic" | "parallel" | "self-loop" | "fan" | "bus" | "bundle";
|
|
70
|
+
type RouteSide = "left" | "right" | "top" | "bottom";
|
|
71
|
+
interface RouteAttachment {
|
|
72
|
+
/** Node-local attachment rectangle, independent of its outer obstacle bounds. */
|
|
73
|
+
readonly bounds: RouteBounds;
|
|
74
|
+
readonly facing?: "inward" | "outward";
|
|
75
|
+
}
|
|
76
|
+
interface EdgeRoutingSettings {
|
|
77
|
+
readonly sourceAttachment?: RouteAttachment;
|
|
78
|
+
readonly targetAttachment?: RouteAttachment;
|
|
79
|
+
readonly sourceSide?: RouteSide;
|
|
80
|
+
readonly targetSide?: RouteSide;
|
|
81
|
+
readonly waypoints?: readonly RoutePoint[];
|
|
82
|
+
/** Explicit shared routing group. Native binary edges describe branches of a net. */
|
|
83
|
+
readonly group?: string;
|
|
84
|
+
}
|
|
85
|
+
interface RoutingSettings {
|
|
86
|
+
/** Native compound geometry is parent-relative; adapters may supply resolved world geometry. */
|
|
87
|
+
readonly coordinateSpace?: "parent" | "world";
|
|
88
|
+
readonly clearance?: number;
|
|
89
|
+
readonly radius?: number;
|
|
90
|
+
readonly edgeSpacing?: number;
|
|
91
|
+
readonly bendPenalty?: number;
|
|
92
|
+
/** Soft cost per crossing; finite costs keep detours proportionate. Default 80. */
|
|
93
|
+
readonly crossingPenalty?: number;
|
|
94
|
+
/** Cost per unit of parallel overlap within edgeSpacing. Default 8. */
|
|
95
|
+
readonly overlapPenalty?: number;
|
|
96
|
+
readonly maxSearchNodes?: number;
|
|
97
|
+
readonly organicIterations?: number;
|
|
98
|
+
readonly edges?: Readonly<Record<string, EdgeRoutingSettings>>;
|
|
99
|
+
}
|
|
100
|
+
interface RoutingMetrics {
|
|
101
|
+
readonly routedEdges: number;
|
|
102
|
+
readonly reusedEdges: number;
|
|
103
|
+
readonly affectedEdges: number;
|
|
104
|
+
readonly searchNodes: number;
|
|
105
|
+
readonly obstacleCandidates: number;
|
|
106
|
+
}
|
|
107
|
+
/** Immutable value. Retain it per canvas; the private state uses structural sharing. */
|
|
108
|
+
interface RoutingSnapshot {
|
|
109
|
+
readonly strategy: string;
|
|
110
|
+
readonly revision: number;
|
|
111
|
+
readonly routes: ReadonlyMap<string, Route>;
|
|
112
|
+
readonly metrics: RoutingMetrics;
|
|
113
|
+
}
|
|
114
|
+
interface RoutingUpdate {
|
|
115
|
+
readonly snapshot: RoutingSnapshot;
|
|
116
|
+
readonly patches: readonly RoutePatch[];
|
|
117
|
+
/** Compare identity before accepting an asynchronous result. */
|
|
118
|
+
readonly base: RoutingSnapshot;
|
|
119
|
+
}
|
|
120
|
+
type RoutingGraph = Graph<unknown, unknown, unknown, unknown>;
|
|
121
|
+
interface RoutingStrategy {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
route(graph: RoutingGraph, settings?: RoutingSettings): RoutingSnapshot | Promise<RoutingSnapshot>;
|
|
124
|
+
update(graph: RoutingGraph, previous: RoutingSnapshot, diff: GraphDiff<unknown, unknown>, settings?: RoutingSettings): RoutingUpdate | Promise<RoutingUpdate>;
|
|
125
|
+
}
|
|
126
|
+
interface NativeRoutingStrategy extends RoutingStrategy {
|
|
127
|
+
readonly id: RouteStyle;
|
|
128
|
+
route(graph: RoutingGraph, settings?: RoutingSettings): RoutingSnapshot;
|
|
129
|
+
update(graph: RoutingGraph, previous: RoutingSnapshot, diff: GraphDiff<unknown, unknown>, settings?: RoutingSettings): RoutingUpdate;
|
|
130
|
+
}
|
|
131
|
+
type RouteBounds = Readonly<EntityRect>;
|
|
132
|
+
//#endregion
|
|
133
|
+
export { RoutingMetrics as _, RouteBounds as a, RoutingStrategy as b, RoutePath as c, RouteSegment as d, RouteSide as f, RoutingGraph as g, RoutingDiagnosticCode as h, RouteAttachment as i, RoutePoint as l, RoutingDiagnostic as m, NativeRoutingStrategy as n, RouteEndpoint as o, RouteStyle as p, Route as r, RoutePatch as s, EdgeRoutingSettings as t, RouteSection as u, RoutingSettings as v, RoutingUpdate as x, RoutingSnapshot as y };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EntityRect, Graph, GraphDiff, Point } from "@statelyai/graph";
|
|
2
|
+
|
|
3
|
+
//#region src/routing/types.d.ts
|
|
4
|
+
type RoutePoint = Readonly<Point>;
|
|
5
|
+
type RouteSegment = {
|
|
6
|
+
readonly kind: "line";
|
|
7
|
+
readonly to: RoutePoint;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "quadratic";
|
|
10
|
+
readonly control: RoutePoint;
|
|
11
|
+
readonly to: RoutePoint;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "cubic";
|
|
14
|
+
readonly control1: RoutePoint;
|
|
15
|
+
readonly control2: RoutePoint;
|
|
16
|
+
readonly to: RoutePoint;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "arc";
|
|
19
|
+
readonly rx: number;
|
|
20
|
+
readonly ry: number;
|
|
21
|
+
readonly rotation: number;
|
|
22
|
+
readonly largeArc: boolean;
|
|
23
|
+
readonly sweep: boolean;
|
|
24
|
+
readonly to: RoutePoint;
|
|
25
|
+
};
|
|
26
|
+
interface RoutePath {
|
|
27
|
+
readonly start: RoutePoint;
|
|
28
|
+
readonly segments: readonly RouteSegment[];
|
|
29
|
+
}
|
|
30
|
+
type RouteEndpoint = {
|
|
31
|
+
readonly kind: "node";
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
readonly port?: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "label";
|
|
36
|
+
readonly edgeId: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "junction";
|
|
39
|
+
readonly id: string;
|
|
40
|
+
};
|
|
41
|
+
interface RouteSection {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly from: RouteEndpoint;
|
|
44
|
+
readonly to: RouteEndpoint;
|
|
45
|
+
readonly path: RoutePath;
|
|
46
|
+
/** Same ID denotes a shared trunk; render it once. */
|
|
47
|
+
readonly sharedId?: string;
|
|
48
|
+
}
|
|
49
|
+
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION";
|
|
50
|
+
interface RoutingDiagnostic {
|
|
51
|
+
readonly code: RoutingDiagnosticCode;
|
|
52
|
+
readonly edgeId: string;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
}
|
|
55
|
+
interface Route {
|
|
56
|
+
readonly edgeId: string;
|
|
57
|
+
readonly sections: readonly RouteSection[];
|
|
58
|
+
readonly status: "routed" | "fallback";
|
|
59
|
+
readonly diagnostics: readonly RoutingDiagnostic[];
|
|
60
|
+
}
|
|
61
|
+
type RoutePatch = {
|
|
62
|
+
readonly op: "set";
|
|
63
|
+
readonly edgeId: string;
|
|
64
|
+
readonly route: Route;
|
|
65
|
+
} | {
|
|
66
|
+
readonly op: "delete";
|
|
67
|
+
readonly edgeId: string;
|
|
68
|
+
};
|
|
69
|
+
type RouteStyle = "straight" | "bezier" | "orthogonal" | "polyline" | "octilinear" | "curved" | "organic" | "parallel" | "self-loop" | "fan" | "bus" | "bundle";
|
|
70
|
+
type RouteSide = "left" | "right" | "top" | "bottom";
|
|
71
|
+
interface RouteAttachment {
|
|
72
|
+
/** Node-local attachment rectangle, independent of its outer obstacle bounds. */
|
|
73
|
+
readonly bounds: RouteBounds;
|
|
74
|
+
readonly facing?: "inward" | "outward";
|
|
75
|
+
}
|
|
76
|
+
interface EdgeRoutingSettings {
|
|
77
|
+
readonly sourceAttachment?: RouteAttachment;
|
|
78
|
+
readonly targetAttachment?: RouteAttachment;
|
|
79
|
+
readonly sourceSide?: RouteSide;
|
|
80
|
+
readonly targetSide?: RouteSide;
|
|
81
|
+
readonly waypoints?: readonly RoutePoint[];
|
|
82
|
+
/** Explicit shared routing group. Native binary edges describe branches of a net. */
|
|
83
|
+
readonly group?: string;
|
|
84
|
+
}
|
|
85
|
+
interface RoutingSettings {
|
|
86
|
+
/** Native compound geometry is parent-relative; adapters may supply resolved world geometry. */
|
|
87
|
+
readonly coordinateSpace?: "parent" | "world";
|
|
88
|
+
readonly clearance?: number;
|
|
89
|
+
readonly radius?: number;
|
|
90
|
+
readonly edgeSpacing?: number;
|
|
91
|
+
readonly bendPenalty?: number;
|
|
92
|
+
/** Soft cost per crossing; finite costs keep detours proportionate. Default 80. */
|
|
93
|
+
readonly crossingPenalty?: number;
|
|
94
|
+
/** Cost per unit of parallel overlap within edgeSpacing. Default 8. */
|
|
95
|
+
readonly overlapPenalty?: number;
|
|
96
|
+
readonly maxSearchNodes?: number;
|
|
97
|
+
readonly organicIterations?: number;
|
|
98
|
+
readonly edges?: Readonly<Record<string, EdgeRoutingSettings>>;
|
|
99
|
+
}
|
|
100
|
+
interface RoutingMetrics {
|
|
101
|
+
readonly routedEdges: number;
|
|
102
|
+
readonly reusedEdges: number;
|
|
103
|
+
readonly affectedEdges: number;
|
|
104
|
+
readonly searchNodes: number;
|
|
105
|
+
readonly obstacleCandidates: number;
|
|
106
|
+
}
|
|
107
|
+
/** Immutable value. Retain it per canvas; the private state uses structural sharing. */
|
|
108
|
+
interface RoutingSnapshot {
|
|
109
|
+
readonly strategy: string;
|
|
110
|
+
readonly revision: number;
|
|
111
|
+
readonly routes: ReadonlyMap<string, Route>;
|
|
112
|
+
readonly metrics: RoutingMetrics;
|
|
113
|
+
}
|
|
114
|
+
interface RoutingUpdate {
|
|
115
|
+
readonly snapshot: RoutingSnapshot;
|
|
116
|
+
readonly patches: readonly RoutePatch[];
|
|
117
|
+
/** Compare identity before accepting an asynchronous result. */
|
|
118
|
+
readonly base: RoutingSnapshot;
|
|
119
|
+
}
|
|
120
|
+
type RoutingGraph = Graph<unknown, unknown, unknown, unknown>;
|
|
121
|
+
interface RoutingStrategy {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
route(graph: RoutingGraph, settings?: RoutingSettings): RoutingSnapshot | Promise<RoutingSnapshot>;
|
|
124
|
+
update(graph: RoutingGraph, previous: RoutingSnapshot, diff: GraphDiff<unknown, unknown>, settings?: RoutingSettings): RoutingUpdate | Promise<RoutingUpdate>;
|
|
125
|
+
}
|
|
126
|
+
interface NativeRoutingStrategy extends RoutingStrategy {
|
|
127
|
+
readonly id: RouteStyle;
|
|
128
|
+
route(graph: RoutingGraph, settings?: RoutingSettings): RoutingSnapshot;
|
|
129
|
+
update(graph: RoutingGraph, previous: RoutingSnapshot, diff: GraphDiff<unknown, unknown>, settings?: RoutingSettings): RoutingUpdate;
|
|
130
|
+
}
|
|
131
|
+
type RouteBounds = Readonly<EntityRect>;
|
|
132
|
+
//#endregion
|
|
133
|
+
export { RoutingMetrics as _, RouteBounds as a, RoutingStrategy as b, RoutePath as c, RouteSegment as d, RouteSide as f, RoutingGraph as g, RoutingDiagnosticCode as h, RouteAttachment as i, RoutePoint as l, RoutingDiagnostic as m, NativeRoutingStrategy as n, RouteEndpoint as o, RouteStyle as p, Route as r, RoutePatch as s, EdgeRoutingSettings as t, RouteSection as u, RoutingSettings as v, RoutingUpdate as x, RoutingSnapshot as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as ElkLayoutCategoryDescription, f as ElkLayoutOptionDescription, l as ElkLayoutAlgorithmDescription, n as ELKConstructorArguments, p as ElkNode, t as ELK, u as ElkLayoutArguments, v as LaidOutElkNode } from "./public-types-
|
|
1
|
+
import { d as ElkLayoutCategoryDescription, f as ElkLayoutOptionDescription, l as ElkLayoutAlgorithmDescription, n as ELKConstructorArguments, p as ElkNode, t as ELK, u as ElkLayoutArguments, v as LaidOutElkNode } from "./public-types-9NCPoqbp.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/elkjs/worker-client.d.ts
|
|
4
4
|
declare class WorkerELK implements ELK {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as ElkLayoutCategoryDescription, f as ElkLayoutOptionDescription, l as ElkLayoutAlgorithmDescription, n as ELKConstructorArguments, p as ElkNode, t as ELK, u as ElkLayoutArguments, v as LaidOutElkNode } from "./public-types-
|
|
1
|
+
import { d as ElkLayoutCategoryDescription, f as ElkLayoutOptionDescription, l as ElkLayoutAlgorithmDescription, n as ELKConstructorArguments, p as ElkNode, t as ELK, u as ElkLayoutArguments, v as LaidOutElkNode } from "./public-types-BcRZoJE3.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/elkjs/worker-client.d.ts
|
|
4
4
|
declare class WorkerELK implements ELK {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Extensible, graph-native layout algorithms for @statelyai/graph",
|
|
5
5
|
"license": "EPL-2.0 OR GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -59,7 +59,12 @@
|
|
|
59
59
|
"import": "./dist/layered/index.mjs",
|
|
60
60
|
"require": "./dist/layered/index.cjs"
|
|
61
61
|
},
|
|
62
|
-
"./package.json": "./package.json"
|
|
62
|
+
"./package.json": "./package.json",
|
|
63
|
+
"./routing": {
|
|
64
|
+
"types": "./src/routing/index.ts",
|
|
65
|
+
"import": "./dist/routing/index.mjs",
|
|
66
|
+
"require": "./dist/routing/index.cjs"
|
|
67
|
+
}
|
|
63
68
|
},
|
|
64
69
|
"publishConfig": {
|
|
65
70
|
"access": "public"
|
|
@@ -102,6 +107,7 @@
|
|
|
102
107
|
"scripts": {
|
|
103
108
|
"build": "tsdown --config tsdown.config.mjs",
|
|
104
109
|
"bench": "vitest bench --run",
|
|
110
|
+
"bench:statecharts": "tsx scripts/bench-statecharts.ts",
|
|
105
111
|
"demo": "portless --name layout vite --config demo/vite.config.ts",
|
|
106
112
|
"demo:build": "vite build --config demo/vite.config.ts",
|
|
107
113
|
"demo:check": "tsx scripts/generate-demo-corpus.ts --check",
|
|
@@ -124,6 +130,11 @@
|
|
|
124
130
|
"validate:package": "publint --pack npm && tsx scripts/smoke-package.ts",
|
|
125
131
|
"storybook": "storybook dev -p 6018 --host 127.0.0.1 --no-open",
|
|
126
132
|
"storybook:build": "storybook build -o dist-storybook",
|
|
127
|
-
"storybook:typecheck": "tsc --project stories/tsconfig.json"
|
|
133
|
+
"storybook:typecheck": "tsc --project stories/tsconfig.json",
|
|
134
|
+
"test:parity:compound": "tsx scripts/check-compound-parity.ts",
|
|
135
|
+
"test:parity:self-loops": "tsx scripts/check-mixed-self-loop-parity.ts",
|
|
136
|
+
"test:parity:flat": "tsx scripts/check-flat-parity.ts",
|
|
137
|
+
"test:parity:model-order": "tsx scripts/check-model-order-parity.ts",
|
|
138
|
+
"test:parity:oracle": "vitest run --config vitest.parity.config.ts"
|
|
128
139
|
}
|
|
129
140
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LayoutError } from "../errors";
|
|
2
2
|
import type { VisualGraph, Point } from "@statelyai/graph";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Legacy sibling edges are parent-relative; native compound output marks world-space edges. */
|
|
5
5
|
function frames<N, E, G, P>(graph: VisualGraph<N, E, G, P>) {
|
|
6
6
|
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
7
7
|
const offsets = new Map<string, Point>();
|
|
@@ -33,6 +33,7 @@ function frames<N, E, G, P>(graph: VisualGraph<N, E, G, P>) {
|
|
|
33
33
|
const target = nodes.get(edge.targetId)!;
|
|
34
34
|
return [
|
|
35
35
|
edge.id,
|
|
36
|
+
(graph as VisualGraph & { edgeCoordinateSpace?: string }).edgeCoordinateSpace !== "world" &&
|
|
36
37
|
(source.parentId ?? null) === (target.parentId ?? null)
|
|
37
38
|
? offset(source.id)
|
|
38
39
|
: { x: 0, y: 0 },
|
|
@@ -44,8 +45,15 @@ function frames<N, E, G, P>(graph: VisualGraph<N, E, G, P>) {
|
|
|
44
45
|
|
|
45
46
|
export function worldGeometry<N, E, G, P>(graph: VisualGraph<N, E, G, P>): VisualGraph<N, E, G, P> {
|
|
46
47
|
const offsets = frames(graph);
|
|
48
|
+
const base = { ...graph } as VisualGraph<N, E, G, P> & {
|
|
49
|
+
compoundRoutes?: unknown;
|
|
50
|
+
compoundGeometry?: unknown;
|
|
51
|
+
};
|
|
52
|
+
// Authoring may change nodes, labels, or paths. Never carry derived full-layout caches.
|
|
53
|
+
delete base.compoundRoutes;
|
|
54
|
+
delete base.compoundGeometry;
|
|
47
55
|
return {
|
|
48
|
-
...
|
|
56
|
+
...base,
|
|
49
57
|
nodes: graph.nodes.map((n) => {
|
|
50
58
|
const p = offsets.nodes.get(n.id)!;
|
|
51
59
|
return p.x === 0 && p.y === 0 ? n : { ...n, x: n.x + p.x, y: n.y + p.y };
|
|
@@ -64,7 +72,10 @@ export function worldGeometry<N, E, G, P>(graph: VisualGraph<N, E, G, P>): Visua
|
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Convert solved world geometry back to parent-relative coordinates, using
|
|
77
|
+
* each parent's solved position. Untouched coordinates stay exactly as authored.
|
|
78
|
+
*/
|
|
68
79
|
export function nativeGeometry<N, E, G, P>(
|
|
69
80
|
graph: VisualGraph<N, E, G, P>,
|
|
70
81
|
original: VisualGraph<N, E, G, P>,
|
|
@@ -75,30 +86,47 @@ export function nativeGeometry<N, E, G, P>(
|
|
|
75
86
|
const originalEdges = new Map(original.edges.map((e) => [e.id, e]));
|
|
76
87
|
const worldNodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
77
88
|
const worldEdges = new Map(world.edges.map((e) => [e.id, e]));
|
|
89
|
+
const solved = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
90
|
+
const frame = (id: string): Point => {
|
|
91
|
+
const parentId = originalNodes.get(id)?.parentId;
|
|
92
|
+
const parent = parentId == null ? undefined : solved.get(parentId);
|
|
93
|
+
return parent ? { x: parent.x, y: parent.y } : { x: 0, y: 0 };
|
|
94
|
+
};
|
|
95
|
+
const same = (a: Point, b: Point) => a.x === b.x && a.y === b.y;
|
|
78
96
|
return {
|
|
79
97
|
...graph,
|
|
80
98
|
nodes: graph.nodes.map((n) => {
|
|
81
|
-
const p =
|
|
99
|
+
const p = frame(n.id),
|
|
82
100
|
before = originalNodes.get(n.id)!,
|
|
83
101
|
prior = worldNodes.get(n.id)!;
|
|
102
|
+
const kept = same(p, offsets.nodes.get(n.id)!);
|
|
84
103
|
return {
|
|
85
104
|
...n,
|
|
86
|
-
x: n.x === prior.x ? before.x : n.x - p.x,
|
|
87
|
-
y: n.y === prior.y ? before.y : n.y - p.y,
|
|
105
|
+
x: kept && n.x === prior.x ? before.x : n.x - p.x,
|
|
106
|
+
y: kept && n.y === prior.y ? before.y : n.y - p.y,
|
|
88
107
|
};
|
|
89
108
|
}),
|
|
90
109
|
edges: graph.edges.map((e) => {
|
|
91
|
-
const
|
|
110
|
+
const old = offsets.edges.get(e.id)!,
|
|
92
111
|
before = originalEdges.get(e.id)!,
|
|
93
112
|
prior = worldEdges.get(e.id)!;
|
|
113
|
+
const source = originalNodes.get(e.sourceId)!,
|
|
114
|
+
target = originalNodes.get(e.targetId)!;
|
|
115
|
+
// Same rule as frames(): sibling edges in legacy space use their parent's frame.
|
|
116
|
+
const p =
|
|
117
|
+
(original as VisualGraph & { edgeCoordinateSpace?: string }).edgeCoordinateSpace !==
|
|
118
|
+
"world" && (source.parentId ?? null) === (target.parentId ?? null)
|
|
119
|
+
? frame(source.id)
|
|
120
|
+
: { x: 0, y: 0 };
|
|
121
|
+
const kept = same(p, old);
|
|
94
122
|
const points =
|
|
95
|
-
JSON.stringify(e.points) === JSON.stringify(prior.points)
|
|
123
|
+
kept && JSON.stringify(e.points) === JSON.stringify(prior.points)
|
|
96
124
|
? before.points
|
|
97
125
|
: e.points?.map((v) => ({ x: v.x - p.x, y: v.y - p.y }));
|
|
98
126
|
return {
|
|
99
127
|
...e,
|
|
100
|
-
x: e.x === prior.x ? before.x : e.x - p.x,
|
|
101
|
-
y: e.y === prior.y ? before.y : e.y - p.y,
|
|
128
|
+
x: kept && e.x === prior.x ? before.x : e.x - p.x,
|
|
129
|
+
y: kept && e.y === prior.y ? before.y : e.y - p.y,
|
|
102
130
|
...(points === undefined ? {} : { points }),
|
|
103
131
|
};
|
|
104
132
|
}),
|
package/src/authoring/partial.ts
CHANGED
|
@@ -92,7 +92,9 @@ export function runPartialLayout<N, E, G, P>(
|
|
|
92
92
|
throw new LayoutError(`Unknown selected entity: ${id}`, "INVALID_SELECTION");
|
|
93
93
|
}
|
|
94
94
|
const containers = new Set(input.nodes.flatMap((n) => (n.parentId == null ? [] : [n.parentId])));
|
|
95
|
-
|
|
95
|
+
// Only the full-layout constraint phase may move containers: the solver
|
|
96
|
+
// keeps their children inside and resizes them to fit.
|
|
97
|
+
if (phase === "selection" && [...selectedNodes].some((id) => containers.has(id)))
|
|
96
98
|
throw new UnsupportedLayoutError(
|
|
97
99
|
"Authoring layout cannot move containers; select leaf nodes or use full layout without geometry constraints",
|
|
98
100
|
);
|
|
@@ -288,7 +290,10 @@ export function runPartialLayout<N, E, G, P>(
|
|
|
288
290
|
geometry === "routes" ? new Set() : selectedEdges,
|
|
289
291
|
context.diagnostics,
|
|
290
292
|
);
|
|
291
|
-
const moved = graph.nodes.filter((n) =>
|
|
293
|
+
const moved = graph.nodes.filter((n) => {
|
|
294
|
+
const before = byId.get(n.id)!;
|
|
295
|
+
return !samePosition(n, before) || n.width !== before.width || n.height !== before.height;
|
|
296
|
+
});
|
|
292
297
|
const movedIds = new Set(moved.map((n) => n.id));
|
|
293
298
|
const resultNodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
294
299
|
const automatic = new Set<string>();
|
package/src/authoring/routing.ts
CHANGED
|
@@ -24,7 +24,10 @@ export function crossesRect(a: Point, b: Point, rect: EntityRect): boolean {
|
|
|
24
24
|
const min = rect[axis],
|
|
25
25
|
max = min + (axis === "x" ? rect.width : rect.height);
|
|
26
26
|
if (Math.abs(delta) < 1e-10) {
|
|
27
|
-
|
|
27
|
+
// Scope transforms can put a boundary point a few ulps inside a
|
|
28
|
+
// rectangle. Match the existing intersection tolerance for parallel
|
|
29
|
+
// segments so legal boundary travel is not reported as penetration.
|
|
30
|
+
if (a[axis] <= min + 1e-10 || a[axis] >= max - 1e-10) return false;
|
|
28
31
|
} else {
|
|
29
32
|
const t1 = (min - a[axis]) / delta,
|
|
30
33
|
t2 = (max - a[axis]) / delta;
|
package/src/constraints.ts
CHANGED
|
@@ -85,6 +85,14 @@ export function solveGeometryConstraints<N, E, G, P>(
|
|
|
85
85
|
const variables = new Map<string, { x: Variable; y: Variable }>();
|
|
86
86
|
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
87
87
|
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
88
|
+
// Movable containers move and resize with their content. Each one keeps its
|
|
89
|
+
// position most strongly, then its children keep their offsets, then the
|
|
90
|
+
// children keep their own positions; children always stay inside.
|
|
91
|
+
const childrenByParent = new Map<string, (typeof graph.nodes)[number][]>();
|
|
92
|
+
for (const node of graph.nodes)
|
|
93
|
+
if (node.parentId != null && nodes.has(node.parentId) && movableNodes.has(node.parentId))
|
|
94
|
+
childrenByParent.set(node.parentId, [...(childrenByParent.get(node.parentId) ?? []), node]);
|
|
95
|
+
const extents = new Map<string, { right: Variable; bottom: Variable }>();
|
|
88
96
|
function geometry(ref: GeometryReference) {
|
|
89
97
|
const node = "nodeId" in ref;
|
|
90
98
|
const id = node ? ref.nodeId : ref.edgeId;
|
|
@@ -95,21 +103,90 @@ export function solveGeometryConstraints<N, E, G, P>(
|
|
|
95
103
|
if (movable && !variables.has(key)) {
|
|
96
104
|
const pair = { x: new Variable(`${key}.x`), y: new Variable(`${key}.y`) };
|
|
97
105
|
variables.set(key, pair);
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
const parentId = node ? nodes.get(id)?.parentId : undefined;
|
|
107
|
+
const stay =
|
|
108
|
+
parentId != null && childrenByParent.has(parentId)
|
|
109
|
+
? Strength.weak / 100
|
|
110
|
+
: Strength.weak / 10;
|
|
111
|
+
solver.addConstraint(new Constraint(pair.x, Operator.Eq, rect.x, stay));
|
|
112
|
+
solver.addConstraint(new Constraint(pair.y, Operator.Eq, rect.y, stay));
|
|
113
|
+
if (node && childrenByParent.has(id)) {
|
|
114
|
+
const extent = {
|
|
115
|
+
right: new Variable(`${key}.right`),
|
|
116
|
+
bottom: new Variable(`${key}.bottom`),
|
|
117
|
+
};
|
|
118
|
+
extents.set(id, extent);
|
|
119
|
+
solver.addConstraint(
|
|
120
|
+
new Constraint(extent.right, Operator.Eq, rect.x + rect.width, Strength.weak / 10),
|
|
121
|
+
);
|
|
122
|
+
solver.addConstraint(
|
|
123
|
+
new Constraint(extent.bottom, Operator.Eq, rect.y + rect.height, Strength.weak / 10),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
100
126
|
}
|
|
101
|
-
return { rect, variables: variables.get(key) };
|
|
127
|
+
return { rect, variables: variables.get(key), extent: node ? extents.get(id) : undefined };
|
|
102
128
|
}
|
|
103
129
|
function expression(ref: GeometryReference, attribute: GeometryAttribute): Expression {
|
|
104
|
-
const { rect, variables: pair } = geometry(ref);
|
|
130
|
+
const { rect, variables: pair, extent: far } = geometry(ref);
|
|
105
131
|
const axis = attribute === "y" || attribute === "centerY" || attribute === "bottom" ? "y" : "x";
|
|
132
|
+
const start = new Expression(pair?.[axis] ?? rect[axis]);
|
|
133
|
+
if (far && attribute !== "x" && attribute !== "y") {
|
|
134
|
+
const end = new Expression(axis === "x" ? far.right : far.bottom);
|
|
135
|
+
return attribute.startsWith("center") ? start.plus(end).divide(2) : end;
|
|
136
|
+
}
|
|
106
137
|
const extent = axis === "x" ? rect.width : rect.height;
|
|
107
138
|
const offset = attribute.startsWith("center")
|
|
108
139
|
? extent / 2
|
|
109
140
|
: attribute === "right" || attribute === "bottom"
|
|
110
141
|
? extent
|
|
111
142
|
: 0;
|
|
112
|
-
return
|
|
143
|
+
return start.plus(offset);
|
|
144
|
+
}
|
|
145
|
+
for (const [parentId, children] of childrenByParent) {
|
|
146
|
+
const parent = geometry({ nodeId: parentId });
|
|
147
|
+
const container = parent.rect;
|
|
148
|
+
// Keep the original padding, header included.
|
|
149
|
+
const pad = {
|
|
150
|
+
left: Math.min(...children.map((child) => child.x - container.x)),
|
|
151
|
+
top: Math.min(...children.map((child) => child.y - container.y)),
|
|
152
|
+
right: Math.min(
|
|
153
|
+
...children.map((child) => container.x + container.width - child.x - child.width),
|
|
154
|
+
),
|
|
155
|
+
bottom: Math.min(
|
|
156
|
+
...children.map((child) => container.y + container.height - child.y - child.height),
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
for (const child of children) {
|
|
160
|
+
const own = geometry({ nodeId: child.id });
|
|
161
|
+
const x = new Expression(own.variables?.x ?? child.x),
|
|
162
|
+
y = new Expression(own.variables?.y ?? child.y);
|
|
163
|
+
const px = new Expression(parent.variables!.x),
|
|
164
|
+
py = new Expression(parent.variables!.y);
|
|
165
|
+
solver.addConstraint(
|
|
166
|
+
new Constraint(x.minus(px), Operator.Eq, child.x - container.x, Strength.weak / 20),
|
|
167
|
+
);
|
|
168
|
+
solver.addConstraint(
|
|
169
|
+
new Constraint(y.minus(py), Operator.Eq, child.y - container.y, Strength.weak / 20),
|
|
170
|
+
);
|
|
171
|
+
const right = expression({ nodeId: child.id }, "right"),
|
|
172
|
+
bottom = expression({ nodeId: child.id }, "bottom");
|
|
173
|
+
solver.addConstraint(new Constraint(x.minus(px), Operator.Ge, Math.max(0, pad.left)));
|
|
174
|
+
solver.addConstraint(new Constraint(y.minus(py), Operator.Ge, Math.max(0, pad.top)));
|
|
175
|
+
solver.addConstraint(
|
|
176
|
+
new Constraint(
|
|
177
|
+
new Expression(parent.extent!.right).minus(right),
|
|
178
|
+
Operator.Ge,
|
|
179
|
+
Math.max(0, pad.right),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
solver.addConstraint(
|
|
183
|
+
new Constraint(
|
|
184
|
+
new Expression(parent.extent!.bottom).minus(bottom),
|
|
185
|
+
Operator.Ge,
|
|
186
|
+
Math.max(0, pad.bottom),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
113
190
|
}
|
|
114
191
|
const checks: { constraint: LayoutConstraint; expression: Expression; operator: Operator }[] = [];
|
|
115
192
|
const ids = new Set<string>();
|
|
@@ -180,6 +257,85 @@ export function solveGeometryConstraints<N, E, G, P>(
|
|
|
180
257
|
}
|
|
181
258
|
}
|
|
182
259
|
solver.updateVariables();
|
|
260
|
+
// Overlap is never traded for a constraint. Separate siblings that now
|
|
261
|
+
// overlap, on the axis that separated them before, until none do; a
|
|
262
|
+
// required constraint that needs the overlap fails instead.
|
|
263
|
+
const box = (node: (typeof graph.nodes)[number]) => {
|
|
264
|
+
const pair = variables.get(JSON.stringify(["node", node.id]));
|
|
265
|
+
const extent = extents.get(node.id);
|
|
266
|
+
const x = pair?.x.value() ?? node.x,
|
|
267
|
+
y = pair?.y.value() ?? node.y;
|
|
268
|
+
return {
|
|
269
|
+
x,
|
|
270
|
+
y,
|
|
271
|
+
width: extent ? extent.right.value() - x : node.width,
|
|
272
|
+
height: extent ? extent.bottom.value() - y : node.height,
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
const siblingGroups = new Map<string | null, (typeof graph.nodes)[number][]>();
|
|
276
|
+
for (const node of graph.nodes)
|
|
277
|
+
siblingGroups.set(node.parentId ?? null, [
|
|
278
|
+
...(siblingGroups.get(node.parentId ?? null) ?? []),
|
|
279
|
+
node,
|
|
280
|
+
]);
|
|
281
|
+
const separated = new Set<string>();
|
|
282
|
+
for (let round = 0; round < 8; round++) {
|
|
283
|
+
let added = false;
|
|
284
|
+
for (const group of siblingGroups.values())
|
|
285
|
+
for (const [index, first] of group.entries())
|
|
286
|
+
for (const second of group.slice(index + 1)) {
|
|
287
|
+
if (!movableNodes.has(first.id) && !movableNodes.has(second.id)) continue;
|
|
288
|
+
const a = box(first),
|
|
289
|
+
b = box(second);
|
|
290
|
+
if (
|
|
291
|
+
a.x >= b.x + b.width ||
|
|
292
|
+
b.x >= a.x + a.width ||
|
|
293
|
+
a.y >= b.y + b.height ||
|
|
294
|
+
b.y >= a.y + a.height
|
|
295
|
+
)
|
|
296
|
+
continue;
|
|
297
|
+
const key = JSON.stringify([first.id, second.id]);
|
|
298
|
+
if (separated.has(key)) continue;
|
|
299
|
+
separated.add(key);
|
|
300
|
+
// Keep the original order on the axis with the larger original gap.
|
|
301
|
+
const gapX = Math.max(
|
|
302
|
+
first.x - second.x - second.width,
|
|
303
|
+
second.x - first.x - first.width,
|
|
304
|
+
);
|
|
305
|
+
const gapY = Math.max(
|
|
306
|
+
first.y - second.y - second.height,
|
|
307
|
+
second.y - first.y - first.height,
|
|
308
|
+
);
|
|
309
|
+
const axis = gapX >= gapY ? "x" : "y";
|
|
310
|
+
const firstLeads =
|
|
311
|
+
axis === "x" ? first.x + first.width <= second.x : first.y + first.height <= second.y;
|
|
312
|
+
const [lead, trail] = firstLeads ? [first, second] : [second, first];
|
|
313
|
+
const end = axis === "x" ? "right" : "bottom";
|
|
314
|
+
const gap = Math.max(0, Math.min(10, Math.max(gapX, gapY)));
|
|
315
|
+
try {
|
|
316
|
+
solver.addConstraint(
|
|
317
|
+
new Constraint(
|
|
318
|
+
expression({ nodeId: trail.id }, axis).minus(expression({ nodeId: lead.id }, end)),
|
|
319
|
+
Operator.Ge,
|
|
320
|
+
gap,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
} catch {
|
|
324
|
+
const blocking = constraints.find(
|
|
325
|
+
(item) => (item.strength ?? "required") === "required",
|
|
326
|
+
);
|
|
327
|
+
if (blocking)
|
|
328
|
+
constraintFailure(blocking, `would make ${first.id} overlap ${second.id}`);
|
|
329
|
+
throw new LayoutError(
|
|
330
|
+
`Cannot separate ${first.id} from ${second.id}`,
|
|
331
|
+
"UNSATISFIED_CONSTRAINT",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
added = true;
|
|
335
|
+
}
|
|
336
|
+
if (!added) break;
|
|
337
|
+
solver.updateVariables();
|
|
338
|
+
}
|
|
183
339
|
const warned = new Set<string>();
|
|
184
340
|
for (const check of checks) {
|
|
185
341
|
const residual = check.expression.value();
|
|
@@ -212,7 +368,19 @@ export function solveGeometryConstraints<N, E, G, P>(
|
|
|
212
368
|
...graph,
|
|
213
369
|
nodes: graph.nodes.map((node) => {
|
|
214
370
|
const pair = variables.get(JSON.stringify(["node", node.id]));
|
|
215
|
-
|
|
371
|
+
if (!pair) return node;
|
|
372
|
+
const x = coordinate(pair.x),
|
|
373
|
+
y = coordinate(pair.y);
|
|
374
|
+
const extent = extents.get(node.id);
|
|
375
|
+
return extent
|
|
376
|
+
? {
|
|
377
|
+
...node,
|
|
378
|
+
x,
|
|
379
|
+
y,
|
|
380
|
+
width: coordinate(extent.right) - x,
|
|
381
|
+
height: coordinate(extent.bottom) - y,
|
|
382
|
+
}
|
|
383
|
+
: { ...node, x, y };
|
|
216
384
|
}),
|
|
217
385
|
edges: graph.edges.map((edge) => {
|
|
218
386
|
const pair = variables.get(JSON.stringify(["label", edge.id]));
|