@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
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { EntityRect, Graph, GraphDiff, Point } from "@statelyai/graph";
|
|
2
2
|
|
|
3
|
+
//#region src/routing/bezier-fit.d.ts
|
|
4
|
+
interface BezierGuide {
|
|
5
|
+
readonly points: readonly RoutePoint[];
|
|
6
|
+
readonly sourceDirection: RouteSide;
|
|
7
|
+
readonly targetDirection: RouteSide;
|
|
8
|
+
readonly path?: RoutePath;
|
|
9
|
+
}
|
|
10
|
+
type BezierEndpoint = {
|
|
11
|
+
kind: "floating";
|
|
12
|
+
bounds: RouteBounds;
|
|
13
|
+
sides: readonly RouteSide[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: "port";
|
|
16
|
+
point: RoutePoint;
|
|
17
|
+
direction: RouteSide;
|
|
18
|
+
escape: RoutePoint;
|
|
19
|
+
owner: RouteBounds;
|
|
20
|
+
};
|
|
21
|
+
declare function trimBezierPathStart(path: string, maximum?: number): string | undefined;
|
|
22
|
+
//#endregion
|
|
3
23
|
//#region src/routing/types.d.ts
|
|
4
24
|
type RoutePoint = Readonly<Point>;
|
|
5
25
|
type RouteSegment = {
|
|
@@ -43,16 +63,26 @@ interface RouteSection {
|
|
|
43
63
|
readonly from: RouteEndpoint;
|
|
44
64
|
readonly to: RouteEndpoint;
|
|
45
65
|
readonly path: RoutePath;
|
|
66
|
+
readonly fromSide?: RouteSide;
|
|
67
|
+
readonly toSide?: RouteSide;
|
|
46
68
|
/** Same ID denotes a shared trunk; render it once. */
|
|
47
69
|
readonly sharedId?: string;
|
|
48
70
|
}
|
|
49
|
-
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION";
|
|
71
|
+
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION" | "LABEL_OVERLAP";
|
|
50
72
|
interface RoutingDiagnostic {
|
|
51
73
|
readonly code: RoutingDiagnosticCode;
|
|
52
74
|
readonly edgeId: string;
|
|
53
75
|
readonly message: string;
|
|
76
|
+
readonly constraint?: "source-tangent" | "target-tangent" | "label-tangent" | "waypoints" | "node-clearance" | "label-clearance" | "monotonicity" | "bundle";
|
|
77
|
+
readonly relatedIds?: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
interface RouteLabelPlacement {
|
|
80
|
+
readonly bounds: RouteBounds;
|
|
81
|
+
readonly anchor: RoutePoint;
|
|
82
|
+
readonly tangent: RoutePoint;
|
|
54
83
|
}
|
|
55
84
|
interface Route {
|
|
85
|
+
readonly label?: RouteLabelPlacement;
|
|
56
86
|
readonly edgeId: string;
|
|
57
87
|
readonly sections: readonly RouteSection[];
|
|
58
88
|
readonly status: "routed" | "fallback";
|
|
@@ -73,7 +103,84 @@ interface RouteAttachment {
|
|
|
73
103
|
readonly bounds: RouteBounds;
|
|
74
104
|
readonly facing?: "inward" | "outward";
|
|
75
105
|
}
|
|
106
|
+
interface BezierRoutingOptions {
|
|
107
|
+
readonly avoidObstacles?: boolean;
|
|
108
|
+
readonly attachmentMode?: "floating" | "orthogonal";
|
|
109
|
+
readonly targetClearance?: number;
|
|
110
|
+
}
|
|
111
|
+
interface BezierEdgeSettings extends BezierRoutingOptions {
|
|
112
|
+
readonly source?: BezierEndpoint;
|
|
113
|
+
readonly target?: Extract<BezierEndpoint, {
|
|
114
|
+
kind: "floating";
|
|
115
|
+
}>;
|
|
116
|
+
readonly guides?: {
|
|
117
|
+
readonly source: BezierGuide;
|
|
118
|
+
readonly target: BezierGuide;
|
|
119
|
+
};
|
|
120
|
+
readonly obstacles?: readonly RouteBounds[];
|
|
121
|
+
readonly terminalBounds?: readonly RouteBounds[];
|
|
122
|
+
readonly labelTangent?: RoutePoint;
|
|
123
|
+
readonly disabled?: boolean;
|
|
124
|
+
}
|
|
125
|
+
type RoutingTangent = {
|
|
126
|
+
readonly kind: "automatic";
|
|
127
|
+
} | {
|
|
128
|
+
readonly kind: "border-normal";
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: "vector";
|
|
131
|
+
readonly direction: RoutePoint;
|
|
132
|
+
};
|
|
133
|
+
type RoutingCurve = {
|
|
134
|
+
readonly kind: "polyline";
|
|
135
|
+
} | {
|
|
136
|
+
readonly kind: "bezier";
|
|
137
|
+
readonly curvature?: number;
|
|
138
|
+
} | {
|
|
139
|
+
readonly kind: "catmull-rom";
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: "rounded-orthogonal";
|
|
142
|
+
readonly radius?: number;
|
|
143
|
+
};
|
|
144
|
+
type RoutingLabel = {
|
|
145
|
+
readonly kind: "follow";
|
|
146
|
+
readonly fraction?: number;
|
|
147
|
+
readonly offset?: number;
|
|
148
|
+
} | {
|
|
149
|
+
readonly kind: "fixed";
|
|
150
|
+
} | {
|
|
151
|
+
readonly kind: "through";
|
|
152
|
+
readonly tangent?: RoutingTangent;
|
|
153
|
+
} | {
|
|
154
|
+
readonly kind: "gap";
|
|
155
|
+
readonly tangent?: RoutingTangent;
|
|
156
|
+
};
|
|
157
|
+
interface RoutingPolicy {
|
|
158
|
+
readonly curve?: RoutingCurve;
|
|
159
|
+
readonly sourceTangent?: RoutingTangent;
|
|
160
|
+
readonly targetTangent?: RoutingTangent;
|
|
161
|
+
readonly label?: RoutingLabel;
|
|
162
|
+
readonly collisions?: {
|
|
163
|
+
readonly nodes?: "avoid" | "allow";
|
|
164
|
+
readonly labels?: "avoid" | "allow";
|
|
165
|
+
readonly crossingPenalty?: number;
|
|
166
|
+
readonly overlapPenalty?: number;
|
|
167
|
+
};
|
|
168
|
+
readonly monotonic?: {
|
|
169
|
+
readonly axis: "x" | "y" | "both";
|
|
170
|
+
readonly strength: "prefer" | "require";
|
|
171
|
+
};
|
|
172
|
+
readonly stability?: {
|
|
173
|
+
readonly kind: "recompute" | "preserve-valid";
|
|
174
|
+
};
|
|
175
|
+
}
|
|
76
176
|
interface EdgeRoutingSettings {
|
|
177
|
+
readonly policy?: RoutingPolicy;
|
|
178
|
+
/** Resolve the source from this edge label in the current graph geometry. */
|
|
179
|
+
readonly sourceLabel?: string;
|
|
180
|
+
readonly sourceSides?: readonly RouteSide[];
|
|
181
|
+
readonly targetSides?: readonly RouteSide[];
|
|
182
|
+
readonly labelSides?: readonly RouteSide[];
|
|
183
|
+
readonly bezier?: BezierEdgeSettings;
|
|
77
184
|
readonly sourceAttachment?: RouteAttachment;
|
|
78
185
|
readonly targetAttachment?: RouteAttachment;
|
|
79
186
|
readonly sourceSide?: RouteSide;
|
|
@@ -83,6 +190,9 @@ interface EdgeRoutingSettings {
|
|
|
83
190
|
readonly group?: string;
|
|
84
191
|
}
|
|
85
192
|
interface RoutingSettings {
|
|
193
|
+
readonly policy?: RoutingPolicy;
|
|
194
|
+
/** Opt into center-tangent Bézier routing and floating attachments. */
|
|
195
|
+
readonly bezier?: BezierRoutingOptions;
|
|
86
196
|
/** Native compound geometry is parent-relative; adapters may supply resolved world geometry. */
|
|
87
197
|
readonly coordinateSpace?: "parent" | "world";
|
|
88
198
|
readonly clearance?: number;
|
|
@@ -130,4 +240,4 @@ interface NativeRoutingStrategy extends RoutingStrategy {
|
|
|
130
240
|
}
|
|
131
241
|
type RouteBounds = Readonly<EntityRect>;
|
|
132
242
|
//#endregion
|
|
133
|
-
export { RoutingMetrics as _,
|
|
243
|
+
export { BezierGuide as A, RoutingPolicy as C, RoutingTangent as D, RoutingStrategy as E, RoutingUpdate as O, RoutingMetrics as S, RoutingSnapshot as T, RoutingCurve as _, Route as a, RoutingGraph as b, RouteEndpoint as c, RoutePath as d, RoutePoint as f, RouteStyle as g, RouteSide as h, NativeRoutingStrategy as i, trimBezierPathStart as j, BezierEndpoint as k, RouteLabelPlacement as l, RouteSegment as m, BezierRoutingOptions as n, RouteAttachment as o, RouteSection as p, EdgeRoutingSettings as r, RouteBounds as s, BezierEdgeSettings as t, RoutePatch as u, RoutingDiagnostic as v, RoutingSettings as w, RoutingLabel as x, RoutingDiagnosticCode as y };
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { EntityRect, Graph, GraphDiff, Point } from "@statelyai/graph";
|
|
2
2
|
|
|
3
|
+
//#region src/routing/bezier-fit.d.ts
|
|
4
|
+
interface BezierGuide {
|
|
5
|
+
readonly points: readonly RoutePoint[];
|
|
6
|
+
readonly sourceDirection: RouteSide;
|
|
7
|
+
readonly targetDirection: RouteSide;
|
|
8
|
+
readonly path?: RoutePath;
|
|
9
|
+
}
|
|
10
|
+
type BezierEndpoint = {
|
|
11
|
+
kind: "floating";
|
|
12
|
+
bounds: RouteBounds;
|
|
13
|
+
sides: readonly RouteSide[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: "port";
|
|
16
|
+
point: RoutePoint;
|
|
17
|
+
direction: RouteSide;
|
|
18
|
+
escape: RoutePoint;
|
|
19
|
+
owner: RouteBounds;
|
|
20
|
+
};
|
|
21
|
+
declare function trimBezierPathStart(path: string, maximum?: number): string | undefined;
|
|
22
|
+
//#endregion
|
|
3
23
|
//#region src/routing/types.d.ts
|
|
4
24
|
type RoutePoint = Readonly<Point>;
|
|
5
25
|
type RouteSegment = {
|
|
@@ -43,16 +63,26 @@ interface RouteSection {
|
|
|
43
63
|
readonly from: RouteEndpoint;
|
|
44
64
|
readonly to: RouteEndpoint;
|
|
45
65
|
readonly path: RoutePath;
|
|
66
|
+
readonly fromSide?: RouteSide;
|
|
67
|
+
readonly toSide?: RouteSide;
|
|
46
68
|
/** Same ID denotes a shared trunk; render it once. */
|
|
47
69
|
readonly sharedId?: string;
|
|
48
70
|
}
|
|
49
|
-
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION";
|
|
71
|
+
type RoutingDiagnosticCode = "MISSING_GEOMETRY" | "MISSING_PORT" | "ROUTE_CONFLICT" | "ROUTE_BLOCKED" | "SEARCH_BUDGET" | "CONSTRAINT_VIOLATION" | "LABEL_OVERLAP";
|
|
50
72
|
interface RoutingDiagnostic {
|
|
51
73
|
readonly code: RoutingDiagnosticCode;
|
|
52
74
|
readonly edgeId: string;
|
|
53
75
|
readonly message: string;
|
|
76
|
+
readonly constraint?: "source-tangent" | "target-tangent" | "label-tangent" | "waypoints" | "node-clearance" | "label-clearance" | "monotonicity" | "bundle";
|
|
77
|
+
readonly relatedIds?: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
interface RouteLabelPlacement {
|
|
80
|
+
readonly bounds: RouteBounds;
|
|
81
|
+
readonly anchor: RoutePoint;
|
|
82
|
+
readonly tangent: RoutePoint;
|
|
54
83
|
}
|
|
55
84
|
interface Route {
|
|
85
|
+
readonly label?: RouteLabelPlacement;
|
|
56
86
|
readonly edgeId: string;
|
|
57
87
|
readonly sections: readonly RouteSection[];
|
|
58
88
|
readonly status: "routed" | "fallback";
|
|
@@ -73,7 +103,84 @@ interface RouteAttachment {
|
|
|
73
103
|
readonly bounds: RouteBounds;
|
|
74
104
|
readonly facing?: "inward" | "outward";
|
|
75
105
|
}
|
|
106
|
+
interface BezierRoutingOptions {
|
|
107
|
+
readonly avoidObstacles?: boolean;
|
|
108
|
+
readonly attachmentMode?: "floating" | "orthogonal";
|
|
109
|
+
readonly targetClearance?: number;
|
|
110
|
+
}
|
|
111
|
+
interface BezierEdgeSettings extends BezierRoutingOptions {
|
|
112
|
+
readonly source?: BezierEndpoint;
|
|
113
|
+
readonly target?: Extract<BezierEndpoint, {
|
|
114
|
+
kind: "floating";
|
|
115
|
+
}>;
|
|
116
|
+
readonly guides?: {
|
|
117
|
+
readonly source: BezierGuide;
|
|
118
|
+
readonly target: BezierGuide;
|
|
119
|
+
};
|
|
120
|
+
readonly obstacles?: readonly RouteBounds[];
|
|
121
|
+
readonly terminalBounds?: readonly RouteBounds[];
|
|
122
|
+
readonly labelTangent?: RoutePoint;
|
|
123
|
+
readonly disabled?: boolean;
|
|
124
|
+
}
|
|
125
|
+
type RoutingTangent = {
|
|
126
|
+
readonly kind: "automatic";
|
|
127
|
+
} | {
|
|
128
|
+
readonly kind: "border-normal";
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: "vector";
|
|
131
|
+
readonly direction: RoutePoint;
|
|
132
|
+
};
|
|
133
|
+
type RoutingCurve = {
|
|
134
|
+
readonly kind: "polyline";
|
|
135
|
+
} | {
|
|
136
|
+
readonly kind: "bezier";
|
|
137
|
+
readonly curvature?: number;
|
|
138
|
+
} | {
|
|
139
|
+
readonly kind: "catmull-rom";
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: "rounded-orthogonal";
|
|
142
|
+
readonly radius?: number;
|
|
143
|
+
};
|
|
144
|
+
type RoutingLabel = {
|
|
145
|
+
readonly kind: "follow";
|
|
146
|
+
readonly fraction?: number;
|
|
147
|
+
readonly offset?: number;
|
|
148
|
+
} | {
|
|
149
|
+
readonly kind: "fixed";
|
|
150
|
+
} | {
|
|
151
|
+
readonly kind: "through";
|
|
152
|
+
readonly tangent?: RoutingTangent;
|
|
153
|
+
} | {
|
|
154
|
+
readonly kind: "gap";
|
|
155
|
+
readonly tangent?: RoutingTangent;
|
|
156
|
+
};
|
|
157
|
+
interface RoutingPolicy {
|
|
158
|
+
readonly curve?: RoutingCurve;
|
|
159
|
+
readonly sourceTangent?: RoutingTangent;
|
|
160
|
+
readonly targetTangent?: RoutingTangent;
|
|
161
|
+
readonly label?: RoutingLabel;
|
|
162
|
+
readonly collisions?: {
|
|
163
|
+
readonly nodes?: "avoid" | "allow";
|
|
164
|
+
readonly labels?: "avoid" | "allow";
|
|
165
|
+
readonly crossingPenalty?: number;
|
|
166
|
+
readonly overlapPenalty?: number;
|
|
167
|
+
};
|
|
168
|
+
readonly monotonic?: {
|
|
169
|
+
readonly axis: "x" | "y" | "both";
|
|
170
|
+
readonly strength: "prefer" | "require";
|
|
171
|
+
};
|
|
172
|
+
readonly stability?: {
|
|
173
|
+
readonly kind: "recompute" | "preserve-valid";
|
|
174
|
+
};
|
|
175
|
+
}
|
|
76
176
|
interface EdgeRoutingSettings {
|
|
177
|
+
readonly policy?: RoutingPolicy;
|
|
178
|
+
/** Resolve the source from this edge label in the current graph geometry. */
|
|
179
|
+
readonly sourceLabel?: string;
|
|
180
|
+
readonly sourceSides?: readonly RouteSide[];
|
|
181
|
+
readonly targetSides?: readonly RouteSide[];
|
|
182
|
+
readonly labelSides?: readonly RouteSide[];
|
|
183
|
+
readonly bezier?: BezierEdgeSettings;
|
|
77
184
|
readonly sourceAttachment?: RouteAttachment;
|
|
78
185
|
readonly targetAttachment?: RouteAttachment;
|
|
79
186
|
readonly sourceSide?: RouteSide;
|
|
@@ -83,6 +190,9 @@ interface EdgeRoutingSettings {
|
|
|
83
190
|
readonly group?: string;
|
|
84
191
|
}
|
|
85
192
|
interface RoutingSettings {
|
|
193
|
+
readonly policy?: RoutingPolicy;
|
|
194
|
+
/** Opt into center-tangent Bézier routing and floating attachments. */
|
|
195
|
+
readonly bezier?: BezierRoutingOptions;
|
|
86
196
|
/** Native compound geometry is parent-relative; adapters may supply resolved world geometry. */
|
|
87
197
|
readonly coordinateSpace?: "parent" | "world";
|
|
88
198
|
readonly clearance?: number;
|
|
@@ -130,4 +240,4 @@ interface NativeRoutingStrategy extends RoutingStrategy {
|
|
|
130
240
|
}
|
|
131
241
|
type RouteBounds = Readonly<EntityRect>;
|
|
132
242
|
//#endregion
|
|
133
|
-
export { RoutingMetrics as _,
|
|
243
|
+
export { BezierGuide as A, RoutingPolicy as C, RoutingTangent as D, RoutingStrategy as E, RoutingUpdate as O, RoutingMetrics as S, RoutingSnapshot as T, RoutingCurve as _, Route as a, RoutingGraph as b, RouteEndpoint as c, RoutePath as d, RoutePoint as f, RouteStyle as g, RouteSide as h, NativeRoutingStrategy as i, trimBezierPathStart as j, BezierEndpoint as k, RouteLabelPlacement as l, RouteSegment as m, BezierRoutingOptions as n, RouteAttachment as o, RouteSection as p, EdgeRoutingSettings as r, RouteBounds as s, BezierEdgeSettings as t, RoutePatch as u, RoutingDiagnostic as v, RoutingSettings as w, RoutingLabel as x, RoutingDiagnosticCode as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -128,6 +128,9 @@
|
|
|
128
128
|
"typecheck:repo": "tsc --project tsconfig.repo.json",
|
|
129
129
|
"verify": "oxfmt --check . && oxlint && tsc --noEmit && tsc --project tsconfig.repo.json && tsc --project demo/tsconfig.json && tsc --project stories/tsconfig.json && tsx scripts/generate-elk-layered-options.ts --check && tsx scripts/generate-demo-corpus.ts --check && vitest --run && tsdown && vite build --config demo/vite.config.ts && storybook build -o dist-storybook --quiet && publint --pack npm && tsx scripts/smoke-package.ts",
|
|
130
130
|
"validate:package": "publint --pack npm && tsx scripts/smoke-package.ts",
|
|
131
|
+
"verify:routing-policies": "tsx scripts/check-routing-policies.ts",
|
|
132
|
+
"routing:gallery": "tsx scripts/render-routing-policies.ts",
|
|
133
|
+
"routing:quality": "tsx scripts/render-routing-quality.ts",
|
|
131
134
|
"storybook": "storybook dev -p 6018 --host 127.0.0.1 --no-open",
|
|
132
135
|
"storybook:build": "storybook build -o dist-storybook",
|
|
133
136
|
"storybook:typecheck": "tsc --project stories/tsconfig.json",
|
package/src/authoring/partial.ts
CHANGED
|
@@ -363,13 +363,7 @@ export function runPartialLayout<N, E, G, P>(
|
|
|
363
363
|
"Partial layout supports its built-in orthogonal router only",
|
|
364
364
|
);
|
|
365
365
|
const ancestorIds = new Set([...ancestors(source.id), ...ancestors(target.id)]);
|
|
366
|
-
const nodeObstacles = graph.nodes
|
|
367
|
-
.filter((n) => !ancestorIds.has(n.id))
|
|
368
|
-
.map((n) =>
|
|
369
|
-
n.id === source.id || n.id === target.id
|
|
370
|
-
? n
|
|
371
|
-
: { ...n, x: n.x - 1, y: n.y - 1, width: n.width + 2, height: n.height + 2 },
|
|
372
|
-
);
|
|
366
|
+
const nodeObstacles = graph.nodes.filter((n) => !ancestorIds.has(n.id));
|
|
373
367
|
const labelObstacles = [...edgeById.values()].filter(
|
|
374
368
|
(e) => e.id !== edge.id && e.width > 0 && e.height > 0,
|
|
375
369
|
);
|
package/src/authoring/routing.ts
CHANGED
|
@@ -191,24 +191,69 @@ export function routeOrthogonal(
|
|
|
191
191
|
waypoints: readonly Point[],
|
|
192
192
|
check: () => void,
|
|
193
193
|
): Point[] | undefined {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
194
|
+
const start = endpoint(source, edge.sourcePort, direction, true);
|
|
195
|
+
const end = endpoint(target, edge.targetPort, direction, false);
|
|
196
|
+
const normal = (node: VisualNode, point: Point): Point => {
|
|
197
|
+
const sides = [
|
|
198
|
+
{ distance: Math.abs(point.x - node.x), x: -1, y: 0 },
|
|
199
|
+
{ distance: Math.abs(point.x - node.x - node.width), x: 1, y: 0 },
|
|
200
|
+
{ distance: Math.abs(point.y - node.y), x: 0, y: -1 },
|
|
201
|
+
{ distance: Math.abs(point.y - node.y - node.height), x: 0, y: 1 },
|
|
202
|
+
];
|
|
203
|
+
return sides.sort((a, b) => a.distance - b.distance)[0]!;
|
|
204
|
+
};
|
|
205
|
+
const startNormal = normal(source, start);
|
|
206
|
+
const endNormal = normal(target, end);
|
|
207
|
+
for (const clearance of [8, 4, 1, 0]) {
|
|
208
|
+
check();
|
|
209
|
+
const padded = obstacles.map((rect) => ({
|
|
210
|
+
...rect,
|
|
211
|
+
x: rect.x - clearance,
|
|
212
|
+
y: rect.y - clearance,
|
|
213
|
+
width: rect.width + clearance * 2,
|
|
214
|
+
height: rect.height + clearance * 2,
|
|
215
|
+
}));
|
|
216
|
+
const startLead = {
|
|
217
|
+
x: start.x + startNormal.x * clearance,
|
|
218
|
+
y: start.y + startNormal.y * clearance,
|
|
219
|
+
};
|
|
220
|
+
const endLead = {
|
|
221
|
+
x: end.x + endNormal.x * clearance,
|
|
222
|
+
y: end.y + endNormal.y * clearance,
|
|
223
|
+
};
|
|
224
|
+
if (
|
|
225
|
+
obstacles.some(
|
|
226
|
+
(rect, i) =>
|
|
227
|
+
(rect !== source && crossesRect(start, startLead, padded[i]!)) ||
|
|
228
|
+
(rect !== target && crossesRect(end, endLead, padded[i]!)),
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
continue;
|
|
232
|
+
const anchors = [startLead, ...waypoints, endLead];
|
|
233
|
+
const points: Point[] = [start];
|
|
234
|
+
let routed = true;
|
|
235
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
236
|
+
const segment = connect(anchors[i - 1]!, anchors[i]!, padded, check);
|
|
237
|
+
if (!segment) {
|
|
238
|
+
routed = false;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
points.push(...(i === 1 ? segment : segment.slice(1)));
|
|
242
|
+
}
|
|
243
|
+
if (!routed) continue;
|
|
244
|
+
points.push(end);
|
|
245
|
+
const unique = points.filter(
|
|
246
|
+
(p, i) => i === 0 || p.x !== points[i - 1]!.x || p.y !== points[i - 1]!.y,
|
|
212
247
|
);
|
|
213
|
-
|
|
248
|
+
return unique.filter((p, i) => {
|
|
249
|
+
if (i === 0 || i === unique.length - 1) return true;
|
|
250
|
+
const before = unique[i - 1]!,
|
|
251
|
+
after = unique[i + 1]!;
|
|
252
|
+
return !(
|
|
253
|
+
(before.x === p.x && p.x === after.x && (p.y - before.y) * (after.y - p.y) >= 0) ||
|
|
254
|
+
(before.y === p.y && p.y === after.y && (p.x - before.x) * (after.x - p.x) >= 0)
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
214
259
|
}
|
package/src/elkjs/index.ts
CHANGED
|
@@ -96,6 +96,31 @@ interface PreparedElkScope {
|
|
|
96
96
|
finish(orders?: ReadonlyMap<PreparedElkScope, LayerOrder>): Promise<ElkNode>;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const LAYOUT_UNITS_PER_PX = 64;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Measured sizes carry float noise (a browser rect divided by zoom), and a
|
|
103
|
+
* layered layout is full of exact ties that noise decides. Snap sizes to the
|
|
104
|
+
* browser's 1/64px layout unit so the same graph always lays out the same way;
|
|
105
|
+
* sizes already on that grid are unchanged.
|
|
106
|
+
*/
|
|
107
|
+
function snapSizes(node: ElkNode): void {
|
|
108
|
+
const snap = (shape: { width?: number; height?: number }) => {
|
|
109
|
+
if (typeof shape.width === "number")
|
|
110
|
+
shape.width = Math.round(shape.width * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
|
|
111
|
+
if (typeof shape.height === "number")
|
|
112
|
+
shape.height = Math.round(shape.height * LAYOUT_UNITS_PER_PX) / LAYOUT_UNITS_PER_PX;
|
|
113
|
+
};
|
|
114
|
+
snap(node);
|
|
115
|
+
for (const label of node.labels ?? []) snap(label);
|
|
116
|
+
for (const port of node.ports ?? []) {
|
|
117
|
+
snap(port);
|
|
118
|
+
for (const label of port.labels ?? []) snap(label);
|
|
119
|
+
}
|
|
120
|
+
for (const edge of node.edges ?? []) for (const label of edge.labels ?? []) snap(label);
|
|
121
|
+
for (const child of node.children ?? []) snapSizes(child);
|
|
122
|
+
}
|
|
123
|
+
|
|
99
124
|
export default class ELK {
|
|
100
125
|
readonly #options: ElkConstructorArguments;
|
|
101
126
|
readonly #algorithmIds: ReadonlySet<string>;
|
|
@@ -171,6 +196,7 @@ export default class ELK {
|
|
|
171
196
|
graph: T,
|
|
172
197
|
arguments_: ElkLayoutArguments = {},
|
|
173
198
|
): Promise<LaidOutElkNode<T>> {
|
|
199
|
+
snapSizes(graph);
|
|
174
200
|
const options = { ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions };
|
|
175
201
|
const seeds = crossingVariants(options, graph);
|
|
176
202
|
const probe = mayShareTracks(graph, options)
|
|
@@ -31,13 +31,15 @@ function opposed(a: readonly Point[], b: readonly Point[]): boolean {
|
|
|
31
31
|
* A reversed feedback edge split by label or inverted-port dummies is routed
|
|
32
32
|
* one segment at a time, so each segment detours to the outer feedback track
|
|
33
33
|
* and back to the next dummy. Rejoin the chain as one track: the label's own
|
|
34
|
-
* track, or the outer track with the label moved onto it, whichever is clear
|
|
34
|
+
* track, or the outer track with the label moved onto it, whichever is clear;
|
|
35
|
+
* failing both, a fresh track past everything else with the label on it.
|
|
35
36
|
*/
|
|
36
37
|
export function straightenFeedbackChains(
|
|
37
38
|
chains: readonly FeedbackChain[],
|
|
38
39
|
pointsByEdgeId: Map<string, readonly Point[]>,
|
|
39
40
|
rectByNodeId: Map<string, EntityRect>,
|
|
40
41
|
horizontal: boolean,
|
|
42
|
+
spacing: number,
|
|
41
43
|
): void {
|
|
42
44
|
const cross = (p: Point) => (horizontal ? p.y : p.x);
|
|
43
45
|
const flow = (p: Point) => (horizontal ? p.x : p.y);
|
|
@@ -60,7 +62,19 @@ export function straightenFeedbackChains(
|
|
|
60
62
|
end = points.at(-1)!;
|
|
61
63
|
const outer = Math.max(...points.map(cross));
|
|
62
64
|
const labelRect = label && rectByNodeId.get(label.id);
|
|
63
|
-
|
|
65
|
+
const tracks = label ? [label.cross, outer] : [outer];
|
|
66
|
+
if (labelRect) {
|
|
67
|
+
// Other feedback routes may share the outer track; past everything, the
|
|
68
|
+
// moved label is clear of them.
|
|
69
|
+
const farthest = Math.max(
|
|
70
|
+
...[...pointsByEdgeId.values()].flatMap((other) => other.map(cross)),
|
|
71
|
+
...[...rectByNodeId.values()].map((rect) =>
|
|
72
|
+
horizontal ? rect.y + rect.height : rect.x + rect.width,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
tracks.push(farthest + spacing + label!.cross - (horizontal ? labelRect.y : labelRect.x));
|
|
76
|
+
}
|
|
77
|
+
for (const track of tracks) {
|
|
64
78
|
const route = [start, lead, at(flow(lead), track), at(flow(tail), track), tail, end];
|
|
65
79
|
const moved = labelRect && {
|
|
66
80
|
...labelRect,
|
|
@@ -103,8 +117,12 @@ export function straightenFeedbackChains(
|
|
|
103
117
|
}
|
|
104
118
|
if (simple.at(-1) !== end) continue;
|
|
105
119
|
if (simple.length >= points.length) break;
|
|
106
|
-
// Never share a track with another route heading the opposite way.
|
|
107
|
-
if (
|
|
120
|
+
// Never newly share a track with another route heading the opposite way.
|
|
121
|
+
if (
|
|
122
|
+
[...pointsByEdgeId].some(
|
|
123
|
+
([id, other]) => id !== edgeId && opposed(simple, other) && !opposed(points, other),
|
|
124
|
+
)
|
|
125
|
+
)
|
|
108
126
|
continue;
|
|
109
127
|
pointsByEdgeId.set(edgeId, simple);
|
|
110
128
|
if (moved) rectByNodeId.set(label!.id, moved);
|