@crazyhappyone/auto-graph 0.2.5 → 0.2.6
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/dist/cli/index.cjs +901 -38
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +901 -38
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +904 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +902 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2249,6 +2249,69 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
2249
2249
|
}
|
|
2250
2250
|
});
|
|
2251
2251
|
}
|
|
2252
|
+
if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
|
|
2253
|
+
distributeSwimlaneChildren(input, boxes, locks, diagnostics);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
|
|
2257
|
+
const spread = input.distributeSwimlaneChildren === "spread";
|
|
2258
|
+
const minGap = input.minSiblingGap ?? 8;
|
|
2259
|
+
for (const swimlane of input.swimlanes) {
|
|
2260
|
+
if (swimlane.layout === "contract") continue;
|
|
2261
|
+
const isVertical = swimlane.orientation !== "horizontal";
|
|
2262
|
+
const axis = isVertical ? "x" : "y";
|
|
2263
|
+
const mainSize = isVertical ? "width" : "height";
|
|
2264
|
+
for (const lane of swimlane.lanes) {
|
|
2265
|
+
if (lane.children.length < 2) continue;
|
|
2266
|
+
const unlocked = [];
|
|
2267
|
+
const reserved = [];
|
|
2268
|
+
for (const childId of lane.children) {
|
|
2269
|
+
const box = boxes.get(childId);
|
|
2270
|
+
if (box === void 0) continue;
|
|
2271
|
+
if (locks.has(childId)) {
|
|
2272
|
+
const lock = locks.get(childId);
|
|
2273
|
+
if (lock.source === "fixed-position") {
|
|
2274
|
+
unlocked.push({ id: childId, box });
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
reserved.push(intervalForBox(box, axis, mainSize));
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
unlocked.push({ id: childId, box });
|
|
2281
|
+
}
|
|
2282
|
+
if (unlocked.length < 2) continue;
|
|
2283
|
+
const contentStart = isVertical ? Math.min(...unlocked.map((c) => c.box.x)) : Math.min(...unlocked.map((c) => c.box.y));
|
|
2284
|
+
const contentEnd = isVertical ? Math.max(...unlocked.map((c) => c.box.x + c.box.width)) : Math.max(...unlocked.map((c) => c.box.y + c.box.height));
|
|
2285
|
+
const contentSpan = contentEnd - contentStart;
|
|
2286
|
+
const totalChildSpan = unlocked.reduce((s, c) => s + c.box[mainSize], 0);
|
|
2287
|
+
const reservedSpan = reserved.reduce((s, r) => s + (r.end - r.start), 0);
|
|
2288
|
+
let effectiveGap = minGap;
|
|
2289
|
+
const remaining = contentSpan - totalChildSpan - reservedSpan - minGap * (unlocked.length - 1);
|
|
2290
|
+
if (spread && remaining > 0) {
|
|
2291
|
+
effectiveGap = minGap + remaining / (unlocked.length - 1);
|
|
2292
|
+
}
|
|
2293
|
+
unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
|
|
2294
|
+
let pos = contentStart;
|
|
2295
|
+
for (const child of unlocked) {
|
|
2296
|
+
pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
|
|
2297
|
+
const newBox = { ...child.box };
|
|
2298
|
+
if (axis === "x") {
|
|
2299
|
+
newBox.x = pos;
|
|
2300
|
+
} else {
|
|
2301
|
+
newBox.y = pos;
|
|
2302
|
+
}
|
|
2303
|
+
boxes.set(child.id, newBox);
|
|
2304
|
+
locks.delete(child.id);
|
|
2305
|
+
pos += child.box[mainSize] + effectiveGap;
|
|
2306
|
+
}
|
|
2307
|
+
diagnostics.push({
|
|
2308
|
+
severity: "info",
|
|
2309
|
+
code: "intra_container_distributed",
|
|
2310
|
+
message: `Distributed ${unlocked.length} children in swimlane ${lane.id} along ${axis}.`,
|
|
2311
|
+
detail: { containerId: lane.id, count: unlocked.length, axis }
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2252
2315
|
}
|
|
2253
2316
|
function intervalForBox(box, axis, mainSize) {
|
|
2254
2317
|
return { start: box[axis], end: box[axis] + box[mainSize] };
|
|
@@ -3661,6 +3724,397 @@ function connectedComponents(nodes, edges) {
|
|
|
3661
3724
|
});
|
|
3662
3725
|
}
|
|
3663
3726
|
|
|
3727
|
+
// src/layout/recursive.ts
|
|
3728
|
+
function buildContainerTree(groups, constraints, edges) {
|
|
3729
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3730
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
3731
|
+
for (const group of groups) {
|
|
3732
|
+
const children = [];
|
|
3733
|
+
for (const nodeId of group.nodeIds) {
|
|
3734
|
+
children.push(nodeId);
|
|
3735
|
+
parentOf.set(nodeId, group.id);
|
|
3736
|
+
}
|
|
3737
|
+
for (const childGroupId of group.groupIds) {
|
|
3738
|
+
children.push(childGroupId);
|
|
3739
|
+
parentOf.set(childGroupId, group.id);
|
|
3740
|
+
}
|
|
3741
|
+
childrenOf.set(group.id, children);
|
|
3742
|
+
}
|
|
3743
|
+
for (const c of constraints) {
|
|
3744
|
+
if (c.kind !== "containment") continue;
|
|
3745
|
+
for (const childId of c.childIds) {
|
|
3746
|
+
const existing = parentOf.get(childId);
|
|
3747
|
+
if (existing !== void 0) {
|
|
3748
|
+
if (existing === c.containerId) continue;
|
|
3749
|
+
const oldSiblings = childrenOf.get(existing) ?? [];
|
|
3750
|
+
const pruned = oldSiblings.filter((id) => id !== childId);
|
|
3751
|
+
if (pruned.length === 0) {
|
|
3752
|
+
childrenOf.delete(existing);
|
|
3753
|
+
} else {
|
|
3754
|
+
childrenOf.set(existing, pruned);
|
|
3755
|
+
}
|
|
3756
|
+
const newSiblings = childrenOf.get(c.containerId) ?? [];
|
|
3757
|
+
newSiblings.push(childId);
|
|
3758
|
+
childrenOf.set(c.containerId, newSiblings);
|
|
3759
|
+
parentOf.set(childId, c.containerId);
|
|
3760
|
+
} else {
|
|
3761
|
+
const list = childrenOf.get(c.containerId) ?? [];
|
|
3762
|
+
list.push(childId);
|
|
3763
|
+
childrenOf.set(c.containerId, list);
|
|
3764
|
+
parentOf.set(childId, c.containerId);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
const rootIds = /* @__PURE__ */ new Set();
|
|
3769
|
+
for (const group of groups) {
|
|
3770
|
+
if (!parentOf.has(group.id)) {
|
|
3771
|
+
rootIds.add(group.id);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
const edgesInGroup = /* @__PURE__ */ new Map();
|
|
3775
|
+
for (const edge of edges) {
|
|
3776
|
+
const srcParent = parentOf.get(edge.source.nodeId);
|
|
3777
|
+
const tgtParent = parentOf.get(edge.target.nodeId);
|
|
3778
|
+
if (srcParent !== void 0 && srcParent === tgtParent) {
|
|
3779
|
+
const list = edgesInGroup.get(srcParent) ?? [];
|
|
3780
|
+
list.push(edge);
|
|
3781
|
+
edgesInGroup.set(srcParent, list);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
const treeDiagnostics = [];
|
|
3785
|
+
return { childrenOf, rootIds, edgesInGroup, diagnostics: treeDiagnostics };
|
|
3786
|
+
}
|
|
3787
|
+
function runRecursiveContainerLayout(input) {
|
|
3788
|
+
const diagnostics = [];
|
|
3789
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
3790
|
+
const groupBoxes = /* @__PURE__ */ new Map();
|
|
3791
|
+
const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
|
|
3792
|
+
const groupById = new Map(input.groups.map((g) => [g.id, g]));
|
|
3793
|
+
const groupIdSet = new Set(input.groups.map((g) => g.id));
|
|
3794
|
+
const {
|
|
3795
|
+
childrenOf,
|
|
3796
|
+
rootIds,
|
|
3797
|
+
edgesInGroup,
|
|
3798
|
+
diagnostics: treeDiagnostics
|
|
3799
|
+
} = buildContainerTree(input.groups, input.constraints, input.edges);
|
|
3800
|
+
diagnostics.push(...treeDiagnostics);
|
|
3801
|
+
if (input.groups.length === 0) {
|
|
3802
|
+
const flat = runComponentAwareDagreInitialLayout({
|
|
3803
|
+
direction: input.direction,
|
|
3804
|
+
nodes: input.nodes.map((n) => ({ id: n.id, size: n.size })),
|
|
3805
|
+
edges: input.edges.map((e) => ({
|
|
3806
|
+
id: e.id,
|
|
3807
|
+
sourceId: e.source.nodeId,
|
|
3808
|
+
targetId: e.target.nodeId
|
|
3809
|
+
})),
|
|
3810
|
+
...input.options === void 0 ? {} : { options: input.options }
|
|
3811
|
+
});
|
|
3812
|
+
diagnostics.push(...flat.diagnostics);
|
|
3813
|
+
for (const [id, box] of flat.boxes) {
|
|
3814
|
+
boxes.set(id, box);
|
|
3815
|
+
}
|
|
3816
|
+
return { boxes, groupBoxes, diagnostics };
|
|
3817
|
+
}
|
|
3818
|
+
const descendants = /* @__PURE__ */ new Map();
|
|
3819
|
+
function collectDescendants(groupId) {
|
|
3820
|
+
const cached = descendants.get(groupId);
|
|
3821
|
+
if (cached !== void 0) return cached;
|
|
3822
|
+
const result = /* @__PURE__ */ new Set();
|
|
3823
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
3824
|
+
for (const childId of children) {
|
|
3825
|
+
result.add(childId);
|
|
3826
|
+
const childDesc = collectDescendants(childId);
|
|
3827
|
+
for (const d of childDesc) result.add(d);
|
|
3828
|
+
}
|
|
3829
|
+
descendants.set(groupId, result);
|
|
3830
|
+
return result;
|
|
3831
|
+
}
|
|
3832
|
+
const groupOrder = topologicalSort(input.groups, childrenOf, groupIdSet);
|
|
3833
|
+
for (const groupId of groupOrder) {
|
|
3834
|
+
const group = groupById.get(groupId);
|
|
3835
|
+
if (group === void 0) continue;
|
|
3836
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
3837
|
+
if (children.length === 0) {
|
|
3838
|
+
const box = {
|
|
3839
|
+
x: 0,
|
|
3840
|
+
y: 0,
|
|
3841
|
+
width: (group.padding?.left ?? 8) + (group.padding?.right ?? 8) + 40,
|
|
3842
|
+
height: (group.padding?.top ?? 8) + (group.padding?.bottom ?? 8) + 20
|
|
3843
|
+
};
|
|
3844
|
+
groupBoxes.set(groupId, box);
|
|
3845
|
+
boxes.set(groupId, box);
|
|
3846
|
+
continue;
|
|
3847
|
+
}
|
|
3848
|
+
const leafNodeIds = [];
|
|
3849
|
+
const nestedGroupIds = [];
|
|
3850
|
+
for (const childId of children) {
|
|
3851
|
+
if (groupIdSet.has(childId)) {
|
|
3852
|
+
nestedGroupIds.push(childId);
|
|
3853
|
+
} else {
|
|
3854
|
+
leafNodeIds.push(childId);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
const childSizes = /* @__PURE__ */ new Map();
|
|
3858
|
+
for (const nodeId of leafNodeIds) {
|
|
3859
|
+
const node = nodeById.get(nodeId);
|
|
3860
|
+
if (node !== void 0) {
|
|
3861
|
+
childSizes.set(nodeId, {
|
|
3862
|
+
width: node.size.width,
|
|
3863
|
+
height: node.size.height
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
for (const nestedId of nestedGroupIds) {
|
|
3868
|
+
const nestedBox = groupBoxes.get(nestedId);
|
|
3869
|
+
if (nestedBox !== void 0) {
|
|
3870
|
+
childSizes.set(nestedId, {
|
|
3871
|
+
width: nestedBox.width,
|
|
3872
|
+
height: nestedBox.height
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
const groupEdges = edgesInGroup.get(groupId) ?? [];
|
|
3877
|
+
const childLayout = runDagreInitialLayout({
|
|
3878
|
+
direction: input.direction,
|
|
3879
|
+
nodes: children.flatMap((childId) => {
|
|
3880
|
+
const size = childSizes.get(childId);
|
|
3881
|
+
return size === void 0 ? [] : [{ id: childId, size }];
|
|
3882
|
+
}),
|
|
3883
|
+
edges: groupEdges.map((e) => ({
|
|
3884
|
+
id: e.id,
|
|
3885
|
+
sourceId: e.source.nodeId,
|
|
3886
|
+
targetId: e.target.nodeId
|
|
3887
|
+
})),
|
|
3888
|
+
options: {
|
|
3889
|
+
...input.options ?? {},
|
|
3890
|
+
ranksep: (input.options?.ranksep ?? 100) * 0.6,
|
|
3891
|
+
// tighter inside containers
|
|
3892
|
+
nodesep: (input.options?.nodesep ?? 80) * 0.6
|
|
3893
|
+
}
|
|
3894
|
+
});
|
|
3895
|
+
diagnostics.push(...childLayout.diagnostics);
|
|
3896
|
+
if (childLayout.boxes.size === 0) continue;
|
|
3897
|
+
const childBoxes = [...childLayout.boxes.values()];
|
|
3898
|
+
const contentBounds = unionBoxes(childBoxes);
|
|
3899
|
+
const padding = group.padding ?? {
|
|
3900
|
+
top: 8,
|
|
3901
|
+
right: 8,
|
|
3902
|
+
bottom: 8,
|
|
3903
|
+
left: 8
|
|
3904
|
+
};
|
|
3905
|
+
const containerBox = {
|
|
3906
|
+
x: 0,
|
|
3907
|
+
y: 0,
|
|
3908
|
+
width: contentBounds.width + (padding.left ?? 8) + (padding.right ?? 8),
|
|
3909
|
+
height: contentBounds.height + (padding.top ?? 8) + (padding.bottom ?? 8)
|
|
3910
|
+
};
|
|
3911
|
+
const offsetX = padding.left ?? 8;
|
|
3912
|
+
const offsetY = padding.top ?? 8;
|
|
3913
|
+
for (const [childId, childBox] of childLayout.boxes) {
|
|
3914
|
+
boxes.set(childId, {
|
|
3915
|
+
...childBox,
|
|
3916
|
+
x: childBox.x + offsetX,
|
|
3917
|
+
y: childBox.y + offsetY
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
groupBoxes.set(groupId, containerBox);
|
|
3921
|
+
boxes.set(groupId, containerBox);
|
|
3922
|
+
}
|
|
3923
|
+
const allContainedIds = /* @__PURE__ */ new Set();
|
|
3924
|
+
for (const [, childIds] of childrenOf) {
|
|
3925
|
+
for (const cid of childIds) allContainedIds.add(cid);
|
|
3926
|
+
}
|
|
3927
|
+
const topLevelNodeIds = /* @__PURE__ */ new Set();
|
|
3928
|
+
for (const node of input.nodes) {
|
|
3929
|
+
if (!allContainedIds.has(node.id)) {
|
|
3930
|
+
topLevelNodeIds.add(node.id);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
const globalNodes = [];
|
|
3934
|
+
for (const nodeId of topLevelNodeIds) {
|
|
3935
|
+
const node = nodeById.get(nodeId);
|
|
3936
|
+
if (node !== void 0) {
|
|
3937
|
+
globalNodes.push({ id: nodeId, size: node.size });
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
for (const rootId of rootIds) {
|
|
3941
|
+
const gb = groupBoxes.get(rootId);
|
|
3942
|
+
if (gb !== void 0) {
|
|
3943
|
+
globalNodes.push({
|
|
3944
|
+
id: rootId,
|
|
3945
|
+
size: { width: gb.width, height: gb.height }
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
function rootContainerOf(id) {
|
|
3950
|
+
for (const rootId of rootIds) {
|
|
3951
|
+
const desc = collectDescendants(rootId);
|
|
3952
|
+
if (desc.has(id)) return rootId;
|
|
3953
|
+
}
|
|
3954
|
+
return void 0;
|
|
3955
|
+
}
|
|
3956
|
+
const globalEdges = input.edges.filter((e) => {
|
|
3957
|
+
for (const group of input.groups) {
|
|
3958
|
+
const desc = collectDescendants(group.id);
|
|
3959
|
+
if (desc.has(e.source.nodeId) && desc.has(e.target.nodeId)) {
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
return true;
|
|
3964
|
+
}).map((e) => ({
|
|
3965
|
+
id: e.id,
|
|
3966
|
+
sourceId: rootContainerOf(e.source.nodeId) ?? e.source.nodeId,
|
|
3967
|
+
targetId: rootContainerOf(e.target.nodeId) ?? e.target.nodeId
|
|
3968
|
+
}));
|
|
3969
|
+
if (globalNodes.length > 0) {
|
|
3970
|
+
const globalLayout = runDagreInitialLayout({
|
|
3971
|
+
direction: input.direction,
|
|
3972
|
+
nodes: globalNodes,
|
|
3973
|
+
edges: globalEdges,
|
|
3974
|
+
...input.options === void 0 ? {} : { options: input.options }
|
|
3975
|
+
});
|
|
3976
|
+
diagnostics.push(...globalLayout.diagnostics);
|
|
3977
|
+
for (const [id, box] of globalLayout.boxes) {
|
|
3978
|
+
if (groupBoxes.has(id)) {
|
|
3979
|
+
groupBoxes.set(id, box);
|
|
3980
|
+
boxes.set(id, box);
|
|
3981
|
+
} else if (topLevelNodeIds.has(id)) {
|
|
3982
|
+
boxes.set(id, box);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
for (const groupId of groupOrder) {
|
|
3986
|
+
const containerBox = groupBoxes.get(groupId);
|
|
3987
|
+
if (containerBox === void 0) continue;
|
|
3988
|
+
const offsetX = containerBox.x;
|
|
3989
|
+
const offsetY = containerBox.y;
|
|
3990
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
3991
|
+
for (const childId of children) {
|
|
3992
|
+
const childBox = boxes.get(childId);
|
|
3993
|
+
if (childBox !== void 0) {
|
|
3994
|
+
boxes.set(childId, {
|
|
3995
|
+
...childBox,
|
|
3996
|
+
x: childBox.x + offsetX,
|
|
3997
|
+
y: childBox.y + offsetY
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
translateDescendants(childId, offsetX, offsetY, boxes, childrenOf);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
return { boxes, groupBoxes, diagnostics };
|
|
4005
|
+
}
|
|
4006
|
+
function translateDescendants(groupId, dx, dy, boxes, childrenOf) {
|
|
4007
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
4008
|
+
for (const childId of children) {
|
|
4009
|
+
const box = boxes.get(childId);
|
|
4010
|
+
if (box !== void 0) {
|
|
4011
|
+
boxes.set(childId, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4012
|
+
}
|
|
4013
|
+
translateDescendants(childId, dx, dy, boxes, childrenOf);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
function topologicalSort(groups, childrenOf, groupIdSet) {
|
|
4017
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4018
|
+
const result = [];
|
|
4019
|
+
function visit(id) {
|
|
4020
|
+
if (visited.has(id)) return;
|
|
4021
|
+
visited.add(id);
|
|
4022
|
+
const children = childrenOf.get(id) ?? [];
|
|
4023
|
+
for (const childId of children) {
|
|
4024
|
+
if (groupIdSet.has(childId)) {
|
|
4025
|
+
visit(childId);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
result.push(id);
|
|
4029
|
+
}
|
|
4030
|
+
for (const group of groups) {
|
|
4031
|
+
visit(group.id);
|
|
4032
|
+
}
|
|
4033
|
+
return result;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
// src/routing/binary-heap.ts
|
|
4037
|
+
var BinaryHeap = class {
|
|
4038
|
+
_data = [];
|
|
4039
|
+
_nextOrder = 0;
|
|
4040
|
+
get size() {
|
|
4041
|
+
return this._data.length;
|
|
4042
|
+
}
|
|
4043
|
+
push(value, priority) {
|
|
4044
|
+
const entry = { value, priority, order: this._nextOrder++ };
|
|
4045
|
+
this._data.push(entry);
|
|
4046
|
+
this._siftUp(this._data.length - 1);
|
|
4047
|
+
}
|
|
4048
|
+
pop() {
|
|
4049
|
+
if (this._data.length === 0) return void 0;
|
|
4050
|
+
const top = this._data[0];
|
|
4051
|
+
const last = this._data.pop();
|
|
4052
|
+
if (this._data.length > 0) {
|
|
4053
|
+
this._data[0] = last;
|
|
4054
|
+
this._siftDown(0);
|
|
4055
|
+
}
|
|
4056
|
+
return top.value;
|
|
4057
|
+
}
|
|
4058
|
+
peek() {
|
|
4059
|
+
return this._data.length > 0 ? this._data[0].value : void 0;
|
|
4060
|
+
}
|
|
4061
|
+
// -----------------------------------------------------------------------
|
|
4062
|
+
// Internals
|
|
4063
|
+
// -----------------------------------------------------------------------
|
|
4064
|
+
_siftUp(idx) {
|
|
4065
|
+
const entry = this._data[idx];
|
|
4066
|
+
while (idx > 0) {
|
|
4067
|
+
const parentIdx = idx - 1 >> 1;
|
|
4068
|
+
const parent = this._data[parentIdx];
|
|
4069
|
+
if (this._less(entry, parent)) {
|
|
4070
|
+
this._data[idx] = parent;
|
|
4071
|
+
idx = parentIdx;
|
|
4072
|
+
} else {
|
|
4073
|
+
break;
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
this._data[idx] = entry;
|
|
4077
|
+
}
|
|
4078
|
+
_siftDown(idx) {
|
|
4079
|
+
const entry = this._data[idx];
|
|
4080
|
+
const size = this._data.length;
|
|
4081
|
+
while (true) {
|
|
4082
|
+
let smallestIdx = idx;
|
|
4083
|
+
const leftIdx = (idx << 1) + 1;
|
|
4084
|
+
const rightIdx = leftIdx + 1;
|
|
4085
|
+
if (leftIdx < size && this._less(
|
|
4086
|
+
this._data[leftIdx],
|
|
4087
|
+
this._data[smallestIdx]
|
|
4088
|
+
)) {
|
|
4089
|
+
smallestIdx = leftIdx;
|
|
4090
|
+
}
|
|
4091
|
+
if (rightIdx < size && this._less(
|
|
4092
|
+
this._data[rightIdx],
|
|
4093
|
+
this._data[smallestIdx]
|
|
4094
|
+
)) {
|
|
4095
|
+
smallestIdx = rightIdx;
|
|
4096
|
+
}
|
|
4097
|
+
if (smallestIdx !== idx) {
|
|
4098
|
+
this._data[idx] = this._data[smallestIdx];
|
|
4099
|
+
idx = smallestIdx;
|
|
4100
|
+
} else {
|
|
4101
|
+
break;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
this._data[idx] = entry;
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Two entries are compared first by priority, then by insertion order
|
|
4108
|
+
* when priorities are equal. The insertion-order tie-break makes the
|
|
4109
|
+
* heap deterministic: for a given sequence of {value, priority} pushes,
|
|
4110
|
+
* the extraction order is always the same.
|
|
4111
|
+
*/
|
|
4112
|
+
_less(a, b) {
|
|
4113
|
+
if (a.priority !== b.priority) return a.priority < b.priority;
|
|
4114
|
+
return a.order < b.order;
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
|
|
3664
4118
|
// src/routing/astar.ts
|
|
3665
4119
|
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3666
4120
|
const margin = options.margin ?? 0;
|
|
@@ -3668,8 +4122,17 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
3668
4122
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3669
4123
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3670
4124
|
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3671
|
-
const
|
|
3672
|
-
const
|
|
4125
|
+
const useCorridor = options.corridorPrefilter ?? true;
|
|
4126
|
+
const corridorMargin = options.corridorMargin ?? 32;
|
|
4127
|
+
const filtered = useCorridor ? filterObstaclesByCorridor(
|
|
4128
|
+
source,
|
|
4129
|
+
target,
|
|
4130
|
+
obstacles,
|
|
4131
|
+
endpointObstacles,
|
|
4132
|
+
corridorMargin
|
|
4133
|
+
) : obstacles;
|
|
4134
|
+
const xs = collectXs(source, target, filtered, margin);
|
|
4135
|
+
const ys = collectYs(source, target, filtered, margin);
|
|
3673
4136
|
if (xs.length * ys.length > maxNodes) {
|
|
3674
4137
|
diagnostics?.push({
|
|
3675
4138
|
severity: "warning",
|
|
@@ -3684,8 +4147,8 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
3684
4147
|
return null;
|
|
3685
4148
|
}
|
|
3686
4149
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
3687
|
-
connectHorizontalEdges(nodes, ys,
|
|
3688
|
-
connectVerticalEdges(nodes, xs,
|
|
4150
|
+
connectHorizontalEdges(nodes, ys, filtered, endpointObstacles, margin);
|
|
4151
|
+
connectVerticalEdges(nodes, xs, filtered, endpointObstacles, margin);
|
|
3689
4152
|
const path = aStarSearch(
|
|
3690
4153
|
nodes,
|
|
3691
4154
|
nodeIndex,
|
|
@@ -3697,6 +4160,22 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
3697
4160
|
if (path === null) return null;
|
|
3698
4161
|
return simplifyRoute(path);
|
|
3699
4162
|
}
|
|
4163
|
+
function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
|
|
4164
|
+
const cx1 = Math.min(source.x, target.x) - margin;
|
|
4165
|
+
const cx2 = Math.max(source.x, target.x) + margin;
|
|
4166
|
+
const cy1 = Math.min(source.y, target.y) - margin;
|
|
4167
|
+
const cy2 = Math.max(source.y, target.y) + margin;
|
|
4168
|
+
const result = [];
|
|
4169
|
+
for (const obs of obstacles) {
|
|
4170
|
+
if (obs.x + obs.width >= cx1 && obs.x <= cx2 && obs.y + obs.height >= cy1 && obs.y <= cy2) {
|
|
4171
|
+
result.push(obs);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
for (const ep of endpointObstacles) {
|
|
4175
|
+
result.push(ep);
|
|
4176
|
+
}
|
|
4177
|
+
return result;
|
|
4178
|
+
}
|
|
3700
4179
|
function collectXs(source, target, obstacles, margin) {
|
|
3701
4180
|
const raw = [];
|
|
3702
4181
|
for (const obs of obstacles) {
|
|
@@ -3840,25 +4319,17 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
|
|
|
3840
4319
|
gScore.set(startId, 0);
|
|
3841
4320
|
const cameFrom = /* @__PURE__ */ new Map();
|
|
3842
4321
|
const cameFromDir = /* @__PURE__ */ new Map();
|
|
3843
|
-
const openSet =
|
|
3844
|
-
openSet.push(
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
for (let i = 1; i < openSet.length; i++) {
|
|
3851
|
-
if (openSet[i].f < openSet[bestIdx].f) {
|
|
3852
|
-
bestIdx = i;
|
|
3853
|
-
}
|
|
3854
|
-
}
|
|
3855
|
-
const current = openSet.splice(bestIdx, 1)[0];
|
|
3856
|
-
if (current.id === goalId) {
|
|
4322
|
+
const openSet = new BinaryHeap();
|
|
4323
|
+
openSet.push(startId, manhattan(source, target));
|
|
4324
|
+
while (openSet.size > 0) {
|
|
4325
|
+
const currentId = openSet.pop();
|
|
4326
|
+
const currentG = gScore.get(currentId);
|
|
4327
|
+
if (currentG === void 0) continue;
|
|
4328
|
+
if (currentId === goalId) {
|
|
3857
4329
|
return reconstructPath(nodes, cameFrom, goalId);
|
|
3858
4330
|
}
|
|
3859
|
-
const node = nodes[
|
|
3860
|
-
const
|
|
3861
|
-
const prevDir = cameFromDir.get(current.id);
|
|
4331
|
+
const node = nodes[currentId];
|
|
4332
|
+
const prevDir = cameFromDir.get(currentId);
|
|
3862
4333
|
for (const [neighborId, edgeCost] of node.neighbors) {
|
|
3863
4334
|
const neighbor = nodes[neighborId];
|
|
3864
4335
|
const tentativeG = currentG + edgeCost * segmentPenalty;
|
|
@@ -3868,10 +4339,10 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
|
|
|
3868
4339
|
const existingG = gScore.get(neighborId);
|
|
3869
4340
|
if (existingG === void 0 || totalG < existingG) {
|
|
3870
4341
|
gScore.set(neighborId, totalG);
|
|
3871
|
-
cameFrom.set(neighborId,
|
|
4342
|
+
cameFrom.set(neighborId, currentId);
|
|
3872
4343
|
cameFromDir.set(neighborId, newDir);
|
|
3873
4344
|
const f = totalG + manhattan(neighbor, target);
|
|
3874
|
-
openSet.push(
|
|
4345
|
+
openSet.push(neighborId, f);
|
|
3875
4346
|
}
|
|
3876
4347
|
}
|
|
3877
4348
|
}
|
|
@@ -3908,6 +4379,292 @@ function areCollinear(a, b, c) {
|
|
|
3908
4379
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
3909
4380
|
}
|
|
3910
4381
|
|
|
4382
|
+
// src/routing/visibility-router.ts
|
|
4383
|
+
function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
|
|
4384
|
+
const margin = options.margin ?? 0;
|
|
4385
|
+
const turnPenalty = options.turnPenalty ?? 50;
|
|
4386
|
+
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
4387
|
+
const endpointObstacles = options.endpointObstacles ?? [];
|
|
4388
|
+
const maxCorners = options.maxCorners ?? 300;
|
|
4389
|
+
const vertices = collectCornerVertices(source, target, obstacles, margin);
|
|
4390
|
+
if (vertices.length > maxCorners) {
|
|
4391
|
+
diagnostics?.push({
|
|
4392
|
+
severity: "warning",
|
|
4393
|
+
code: "routing.visibility.corner_overflow",
|
|
4394
|
+
message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
|
|
4395
|
+
detail: { vertexCount: vertices.length, maxCorners }
|
|
4396
|
+
});
|
|
4397
|
+
return null;
|
|
4398
|
+
}
|
|
4399
|
+
if (obstacles.length === 0) {
|
|
4400
|
+
return simplifyRoute2([source, target]);
|
|
4401
|
+
}
|
|
4402
|
+
const expandedObstacles = margin === 0 ? obstacles : obstacles.map((o) => expandBox2(o, margin));
|
|
4403
|
+
const edges = buildVisibilityEdges(
|
|
4404
|
+
vertices,
|
|
4405
|
+
expandedObstacles,
|
|
4406
|
+
endpointObstacles
|
|
4407
|
+
);
|
|
4408
|
+
const path = aStarSearch2(
|
|
4409
|
+
vertices,
|
|
4410
|
+
edges,
|
|
4411
|
+
source,
|
|
4412
|
+
target,
|
|
4413
|
+
segmentPenalty,
|
|
4414
|
+
turnPenalty
|
|
4415
|
+
);
|
|
4416
|
+
if (path === null) return null;
|
|
4417
|
+
return simplifyRoute2(path, expandedObstacles);
|
|
4418
|
+
}
|
|
4419
|
+
function collectCornerVertices(source, target, obstacles, margin = 0) {
|
|
4420
|
+
const vertices = [
|
|
4421
|
+
{ point: { x: source.x, y: source.y }, obstacleIndex: -1 },
|
|
4422
|
+
{ point: { x: target.x, y: target.y }, obstacleIndex: -1 }
|
|
4423
|
+
];
|
|
4424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4425
|
+
seen.add(`${source.x},${source.y}`);
|
|
4426
|
+
seen.add(`${target.x},${target.y}`);
|
|
4427
|
+
const addVertex = (p, obstacleIndex) => {
|
|
4428
|
+
const key = `${p.x},${p.y}`;
|
|
4429
|
+
if (seen.has(key)) return;
|
|
4430
|
+
seen.add(key);
|
|
4431
|
+
vertices.push({ point: p, obstacleIndex });
|
|
4432
|
+
};
|
|
4433
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
4434
|
+
const obs = obstacles[i];
|
|
4435
|
+
const c0 = { x: obs.x - margin, y: obs.y - margin };
|
|
4436
|
+
const c1 = { x: obs.x + obs.width + margin, y: obs.y - margin };
|
|
4437
|
+
const c2 = {
|
|
4438
|
+
x: obs.x + obs.width + margin,
|
|
4439
|
+
y: obs.y + obs.height + margin
|
|
4440
|
+
};
|
|
4441
|
+
const c3 = { x: obs.x - margin, y: obs.y + obs.height + margin };
|
|
4442
|
+
for (const c of [c0, c1, c2, c3]) {
|
|
4443
|
+
addVertex(c, i);
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
for (const p of [source, target]) {
|
|
4447
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
4448
|
+
const obs = obstacles[i];
|
|
4449
|
+
addVertex({ x: obs.x - margin, y: p.y }, i);
|
|
4450
|
+
addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
|
|
4451
|
+
addVertex({ x: p.x, y: obs.y - margin }, i);
|
|
4452
|
+
addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
|
|
4453
|
+
if (p.x > obs.x && p.x < obs.x + obs.width) {
|
|
4454
|
+
addVertex({ x: p.x, y: obs.y - margin }, i);
|
|
4455
|
+
addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
|
|
4456
|
+
}
|
|
4457
|
+
if (p.y > obs.y && p.y < obs.y + obs.height) {
|
|
4458
|
+
addVertex({ x: obs.x - margin, y: p.y }, i);
|
|
4459
|
+
addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
return vertices;
|
|
4464
|
+
}
|
|
4465
|
+
function buildVisibilityEdges(vertices, obstacles, endpointObstacles) {
|
|
4466
|
+
const edges = [];
|
|
4467
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
4468
|
+
const v = vertices[i];
|
|
4469
|
+
const right = visibleInDirection(
|
|
4470
|
+
v,
|
|
4471
|
+
vertices,
|
|
4472
|
+
obstacles,
|
|
4473
|
+
endpointObstacles,
|
|
4474
|
+
"right"
|
|
4475
|
+
);
|
|
4476
|
+
const left = visibleInDirection(
|
|
4477
|
+
v,
|
|
4478
|
+
vertices,
|
|
4479
|
+
obstacles,
|
|
4480
|
+
endpointObstacles,
|
|
4481
|
+
"left"
|
|
4482
|
+
);
|
|
4483
|
+
const down = visibleInDirection(
|
|
4484
|
+
v,
|
|
4485
|
+
vertices,
|
|
4486
|
+
obstacles,
|
|
4487
|
+
endpointObstacles,
|
|
4488
|
+
"down"
|
|
4489
|
+
);
|
|
4490
|
+
const up = visibleInDirection(
|
|
4491
|
+
v,
|
|
4492
|
+
vertices,
|
|
4493
|
+
obstacles,
|
|
4494
|
+
endpointObstacles,
|
|
4495
|
+
"up"
|
|
4496
|
+
);
|
|
4497
|
+
for (const neighbor of [right, left, down, up]) {
|
|
4498
|
+
if (neighbor !== null && neighbor.index > i) {
|
|
4499
|
+
edges.push({
|
|
4500
|
+
from: i,
|
|
4501
|
+
to: neighbor.index,
|
|
4502
|
+
cost: neighbor.distance
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
return edges;
|
|
4508
|
+
}
|
|
4509
|
+
function visibleInDirection(origin, vertices, obstacles, endpointObstacles, dir) {
|
|
4510
|
+
const candidates = [];
|
|
4511
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
4512
|
+
const v = vertices[i];
|
|
4513
|
+
const dx = v.point.x - origin.point.x;
|
|
4514
|
+
const dy = v.point.y - origin.point.y;
|
|
4515
|
+
switch (dir) {
|
|
4516
|
+
case "right":
|
|
4517
|
+
if (dx > 0 && dy === 0) candidates.push({ index: i, dist: dx });
|
|
4518
|
+
break;
|
|
4519
|
+
case "left":
|
|
4520
|
+
if (dx < 0 && dy === 0) candidates.push({ index: i, dist: -dx });
|
|
4521
|
+
break;
|
|
4522
|
+
case "down":
|
|
4523
|
+
if (dy > 0 && dx === 0) candidates.push({ index: i, dist: dy });
|
|
4524
|
+
break;
|
|
4525
|
+
case "up":
|
|
4526
|
+
if (dy < 0 && dx === 0) candidates.push({ index: i, dist: -dy });
|
|
4527
|
+
break;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
candidates.sort((a, b) => a.dist - b.dist);
|
|
4531
|
+
for (const c of candidates) {
|
|
4532
|
+
if (isSegmentVisible(
|
|
4533
|
+
origin.point,
|
|
4534
|
+
vertices[c.index].point,
|
|
4535
|
+
obstacles,
|
|
4536
|
+
endpointObstacles,
|
|
4537
|
+
origin.obstacleIndex,
|
|
4538
|
+
vertices[c.index].obstacleIndex
|
|
4539
|
+
)) {
|
|
4540
|
+
return { index: c.index, distance: c.dist };
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
return null;
|
|
4544
|
+
}
|
|
4545
|
+
function isSegmentVisible(a, b, obstacles, endpointObstacles, _aObsIdx, _bObsIdx) {
|
|
4546
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
4547
|
+
if (segmentEntersBox(a, b, obstacles[i])) return false;
|
|
4548
|
+
}
|
|
4549
|
+
for (const ep of endpointObstacles) {
|
|
4550
|
+
if (segmentEntersBox(a, b, ep)) return false;
|
|
4551
|
+
}
|
|
4552
|
+
return true;
|
|
4553
|
+
}
|
|
4554
|
+
function segmentEntersBox(start, end, box) {
|
|
4555
|
+
const left = box.x;
|
|
4556
|
+
const right = box.x + box.width;
|
|
4557
|
+
const top = box.y;
|
|
4558
|
+
const bottom = box.y + box.height;
|
|
4559
|
+
const inside = (p) => p.x > left && p.x < right && p.y > top && p.y < bottom;
|
|
4560
|
+
if (inside(start) || inside(end)) return true;
|
|
4561
|
+
if (start.x === end.x) {
|
|
4562
|
+
return start.x > left && start.x < right && Math.max(start.y, end.y) > top && Math.min(start.y, end.y) < bottom;
|
|
4563
|
+
}
|
|
4564
|
+
if (start.y === end.y) {
|
|
4565
|
+
return start.y > top && start.y < bottom && Math.max(start.x, end.x) > left && Math.min(start.x, end.x) < right;
|
|
4566
|
+
}
|
|
4567
|
+
return false;
|
|
4568
|
+
}
|
|
4569
|
+
function expandBox2(box, margin) {
|
|
4570
|
+
return {
|
|
4571
|
+
x: box.x - margin,
|
|
4572
|
+
y: box.y - margin,
|
|
4573
|
+
width: box.width + margin * 2,
|
|
4574
|
+
height: box.height + margin * 2
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
function aStarSearch2(vertices, edges, source, target, segmentPenalty, turnPenalty) {
|
|
4578
|
+
const startId = 0;
|
|
4579
|
+
const goalId = 1;
|
|
4580
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
4581
|
+
gScore.set(startId, 0);
|
|
4582
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
4583
|
+
const cameFromDir = /* @__PURE__ */ new Map();
|
|
4584
|
+
const openSet = new BinaryHeap();
|
|
4585
|
+
openSet.push(startId, manhattan2(source, target));
|
|
4586
|
+
const neighborMap = /* @__PURE__ */ new Map();
|
|
4587
|
+
for (const e of edges) {
|
|
4588
|
+
let list = neighborMap.get(e.from);
|
|
4589
|
+
if (list === void 0) {
|
|
4590
|
+
list = [];
|
|
4591
|
+
neighborMap.set(e.from, list);
|
|
4592
|
+
}
|
|
4593
|
+
list.push({ to: e.to, cost: e.cost });
|
|
4594
|
+
list = neighborMap.get(e.to);
|
|
4595
|
+
if (list === void 0) {
|
|
4596
|
+
list = [];
|
|
4597
|
+
neighborMap.set(e.to, list);
|
|
4598
|
+
}
|
|
4599
|
+
list.push({ to: e.from, cost: e.cost });
|
|
4600
|
+
}
|
|
4601
|
+
while (openSet.size > 0) {
|
|
4602
|
+
const currentId = openSet.pop();
|
|
4603
|
+
const currentG = gScore.get(currentId);
|
|
4604
|
+
if (currentG === void 0) continue;
|
|
4605
|
+
if (currentId === goalId) {
|
|
4606
|
+
return reconstructPath2(vertices, cameFrom, goalId);
|
|
4607
|
+
}
|
|
4608
|
+
const prevDir = cameFromDir.get(currentId);
|
|
4609
|
+
const neighbors = neighborMap.get(currentId) ?? [];
|
|
4610
|
+
for (const { to, cost } of neighbors) {
|
|
4611
|
+
const tentativeG = currentG + cost * segmentPenalty;
|
|
4612
|
+
const toV = vertices[to];
|
|
4613
|
+
const curV = vertices[currentId];
|
|
4614
|
+
const newDir = toV.point.y === curV.point.y ? "h" : "v";
|
|
4615
|
+
const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
|
|
4616
|
+
const totalG = tentativeG + turnCost;
|
|
4617
|
+
const existingG = gScore.get(to);
|
|
4618
|
+
if (existingG === void 0 || totalG < existingG) {
|
|
4619
|
+
gScore.set(to, totalG);
|
|
4620
|
+
cameFrom.set(to, currentId);
|
|
4621
|
+
cameFromDir.set(to, newDir);
|
|
4622
|
+
const f = totalG + manhattan2(toV.point, target);
|
|
4623
|
+
openSet.push(to, f);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
return null;
|
|
4628
|
+
}
|
|
4629
|
+
function manhattan2(a, b) {
|
|
4630
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
4631
|
+
}
|
|
4632
|
+
function reconstructPath2(vertices, cameFrom, goalId) {
|
|
4633
|
+
const path = [];
|
|
4634
|
+
let current = goalId;
|
|
4635
|
+
while (current !== void 0) {
|
|
4636
|
+
const v = vertices[current];
|
|
4637
|
+
path.unshift({ x: v.point.x, y: v.point.y });
|
|
4638
|
+
current = cameFrom.get(current);
|
|
4639
|
+
}
|
|
4640
|
+
return path;
|
|
4641
|
+
}
|
|
4642
|
+
function simplifyRoute2(points, obstacles = []) {
|
|
4643
|
+
if (points.length <= 2) return [...points];
|
|
4644
|
+
const result = [points[0]];
|
|
4645
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
4646
|
+
const prev = result[result.length - 1];
|
|
4647
|
+
const curr = points[i];
|
|
4648
|
+
const next = points[i + 1];
|
|
4649
|
+
const collinear = prev.x === curr.x && curr.x === next.x || prev.y === curr.y && curr.y === next.y;
|
|
4650
|
+
if (!collinear) {
|
|
4651
|
+
result.push(curr);
|
|
4652
|
+
continue;
|
|
4653
|
+
}
|
|
4654
|
+
if (segmentCrossesAnyObstacle(prev, next, obstacles)) {
|
|
4655
|
+
result.push(curr);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
result.push(points[points.length - 1]);
|
|
4659
|
+
return result;
|
|
4660
|
+
}
|
|
4661
|
+
function segmentCrossesAnyObstacle(a, b, obstacles) {
|
|
4662
|
+
for (const obs of obstacles) {
|
|
4663
|
+
if (segmentEntersBox(a, b, obs)) return true;
|
|
4664
|
+
}
|
|
4665
|
+
return false;
|
|
4666
|
+
}
|
|
4667
|
+
|
|
3911
4668
|
// src/routing/routes.ts
|
|
3912
4669
|
function checkBacktracking(points, source, target, diagnostics) {
|
|
3913
4670
|
if (points.length < 2) return;
|
|
@@ -3997,13 +4754,18 @@ function routeEdge(input) {
|
|
|
3997
4754
|
input.source.center,
|
|
3998
4755
|
targetAnchor
|
|
3999
4756
|
);
|
|
4000
|
-
const
|
|
4757
|
+
const cornerPath = findCornerGraphPath(
|
|
4001
4758
|
source,
|
|
4002
4759
|
target,
|
|
4003
4760
|
[...softObstacles, ...hardObstacles],
|
|
4004
|
-
{
|
|
4005
|
-
|
|
4006
|
-
|
|
4761
|
+
{ endpointObstacles, margin: 2 },
|
|
4762
|
+
diagnostics
|
|
4763
|
+
);
|
|
4764
|
+
const path = cornerPath ?? findObstacleFreePath(
|
|
4765
|
+
source,
|
|
4766
|
+
target,
|
|
4767
|
+
[...softObstacles, ...hardObstacles],
|
|
4768
|
+
{ endpointObstacles, margin: 0 },
|
|
4007
4769
|
diagnostics
|
|
4008
4770
|
);
|
|
4009
4771
|
if (path !== null && path.length >= 2) {
|
|
@@ -4246,7 +5008,7 @@ function routeEdge(input) {
|
|
|
4246
5008
|
};
|
|
4247
5009
|
}
|
|
4248
5010
|
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4249
|
-
const simplified =
|
|
5011
|
+
const simplified = simplifyRoute3(points);
|
|
4250
5012
|
if (simplified.length >= 3) {
|
|
4251
5013
|
return simplified;
|
|
4252
5014
|
}
|
|
@@ -4550,7 +5312,7 @@ function squaredDistance2(a, b) {
|
|
|
4550
5312
|
const dy = a.y - b.y;
|
|
4551
5313
|
return dx * dx + dy * dy;
|
|
4552
5314
|
}
|
|
4553
|
-
function
|
|
5315
|
+
function simplifyRoute3(points) {
|
|
4554
5316
|
const withoutDuplicates = [];
|
|
4555
5317
|
for (const point2 of points) {
|
|
4556
5318
|
const previous = withoutDuplicates.at(-1);
|
|
@@ -4890,7 +5652,21 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4890
5652
|
);
|
|
4891
5653
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
4892
5654
|
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
4893
|
-
const
|
|
5655
|
+
const useRecursive = options.recursiveLayout === true;
|
|
5656
|
+
if (useRecursive && initialLayoutMode === "positions") {
|
|
5657
|
+
diagnostics.push({
|
|
5658
|
+
severity: "warning",
|
|
5659
|
+
code: "layout.recursive-ignores-positions",
|
|
5660
|
+
message: 'recursiveLayout overrides initialLayout "positions" \u2014 seed positions are ignored for bottom-up container layout.'
|
|
5661
|
+
});
|
|
5662
|
+
}
|
|
5663
|
+
const layout2 = useRecursive ? runRecursiveContainerLayout({
|
|
5664
|
+
direction: diagram.direction,
|
|
5665
|
+
nodes: styledNodes,
|
|
5666
|
+
groups: styledGroups,
|
|
5667
|
+
edges: styledEdges,
|
|
5668
|
+
constraints
|
|
5669
|
+
}) : runInitialLayout({
|
|
4894
5670
|
mode: initialLayoutMode,
|
|
4895
5671
|
componentAware: options.maxStackDepth === void 0,
|
|
4896
5672
|
direction: diagram.direction,
|
|
@@ -4906,12 +5682,32 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4906
5682
|
options,
|
|
4907
5683
|
diagnostics
|
|
4908
5684
|
);
|
|
5685
|
+
if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
|
|
5686
|
+
const rewrapped = wrapHorizontalStackIfNeeded(
|
|
5687
|
+
initialNodeBoxes,
|
|
5688
|
+
styledNodes,
|
|
5689
|
+
diagram.direction,
|
|
5690
|
+
options,
|
|
5691
|
+
diagnostics
|
|
5692
|
+
);
|
|
5693
|
+
for (const [id, box] of rewrapped) {
|
|
5694
|
+
initialNodeBoxes.set(id, box);
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
if (useRecursive && "groupBoxes" in layout2) {
|
|
5698
|
+
const recursiveLayout = layout2;
|
|
5699
|
+
for (const [groupId, groupBox] of recursiveLayout.groupBoxes) {
|
|
5700
|
+
initialNodeBoxes.set(groupId, groupBox);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
4909
5703
|
expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
|
|
4910
5704
|
const constrained = applyLayoutConstraints({
|
|
4911
5705
|
direction: diagram.direction,
|
|
4912
5706
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
4913
5707
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
4914
5708
|
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
5709
|
+
distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
|
|
5710
|
+
swimlanes: styledSwimlanes,
|
|
4915
5711
|
boxes: initialNodeBoxes,
|
|
4916
5712
|
nodes: styledNodes,
|
|
4917
5713
|
constraints
|
|
@@ -5571,7 +6367,7 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
|
|
|
5571
6367
|
);
|
|
5572
6368
|
return wrapped;
|
|
5573
6369
|
}
|
|
5574
|
-
if (edges.length > 0 || !
|
|
6370
|
+
if (edges.length > 0 || !isStackRunaway(wrapped, nodes, direction, options)) {
|
|
5575
6371
|
reportVerticalRunaway(
|
|
5576
6372
|
wrapped,
|
|
5577
6373
|
nodes,
|
|
@@ -5622,8 +6418,57 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
|
|
|
5622
6418
|
});
|
|
5623
6419
|
return wrapped;
|
|
5624
6420
|
}
|
|
6421
|
+
function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnostics) {
|
|
6422
|
+
if (!isStackRunaway(boxes, nodes, direction, options)) {
|
|
6423
|
+
return new Map(boxes);
|
|
6424
|
+
}
|
|
6425
|
+
const maxRowDepth = options.maxRowDepth;
|
|
6426
|
+
if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
|
|
6427
|
+
return new Map(boxes);
|
|
6428
|
+
}
|
|
6429
|
+
const ordered = [...nodes].sort((a, b) => {
|
|
6430
|
+
const ba = boxes.get(a.id);
|
|
6431
|
+
const bb = boxes.get(b.id);
|
|
6432
|
+
if (ba === void 0 || bb === void 0) return 0;
|
|
6433
|
+
const dx = ba.x - bb.x;
|
|
6434
|
+
return dx !== 0 ? dx : ba.y - bb.y;
|
|
6435
|
+
});
|
|
6436
|
+
const rows = Math.ceil(ordered.length / maxRowDepth);
|
|
6437
|
+
const wrapped = new Map(boxes);
|
|
6438
|
+
const rowSpacing = options.overlapSpacing ?? 40;
|
|
6439
|
+
let minX = Infinity;
|
|
6440
|
+
let minY = Infinity;
|
|
6441
|
+
let maxH = 0;
|
|
6442
|
+
for (const n of ordered) {
|
|
6443
|
+
const b = boxes.get(n.id);
|
|
6444
|
+
if (b !== void 0) {
|
|
6445
|
+
minX = Math.min(minX, b.x);
|
|
6446
|
+
minY = Math.min(minY, b.y);
|
|
6447
|
+
maxH = Math.max(maxH, b.height);
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
for (let ri = 0; ri < rows; ri++) {
|
|
6451
|
+
const rowNodes = ordered.slice(ri * maxRowDepth, (ri + 1) * maxRowDepth);
|
|
6452
|
+
let x = minX;
|
|
6453
|
+
const y = minY + ri * (maxH + rowSpacing);
|
|
6454
|
+
for (const node of rowNodes) {
|
|
6455
|
+
const box = boxes.get(node.id);
|
|
6456
|
+
if (box === void 0) continue;
|
|
6457
|
+
wrapped.set(node.id, { ...box, x, y });
|
|
6458
|
+
x += box.width + rowSpacing;
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
diagnostics.push({
|
|
6462
|
+
severity: "warning",
|
|
6463
|
+
code: "horizontal_runaway",
|
|
6464
|
+
message: `Single-row layout exceeded maxRowDepth ${maxRowDepth}; wrapped into ${rows} rows.`,
|
|
6465
|
+
path: ["nodes"],
|
|
6466
|
+
detail: { nodeCount: ordered.length, maxRowDepth, rows }
|
|
6467
|
+
});
|
|
6468
|
+
return wrapped;
|
|
6469
|
+
}
|
|
5625
6470
|
function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
|
|
5626
|
-
if (!
|
|
6471
|
+
if (!isStackRunaway(boxes, nodes, direction, options)) {
|
|
5627
6472
|
return;
|
|
5628
6473
|
}
|
|
5629
6474
|
diagnostics.push({
|
|
@@ -5639,7 +6484,7 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
|
|
|
5639
6484
|
}
|
|
5640
6485
|
});
|
|
5641
6486
|
}
|
|
5642
|
-
function
|
|
6487
|
+
function isStackRunaway(boxes, nodes, direction, options) {
|
|
5643
6488
|
if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
|
|
5644
6489
|
return false;
|
|
5645
6490
|
}
|
|
@@ -6230,7 +7075,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
6230
7075
|
});
|
|
6231
7076
|
continue;
|
|
6232
7077
|
}
|
|
6233
|
-
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
|
|
7078
|
+
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
|
|
6234
7079
|
const geometry = computeShapeGeometry({
|
|
6235
7080
|
shape: node.shape,
|
|
6236
7081
|
box,
|
|
@@ -6324,7 +7169,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
|
|
|
6324
7169
|
}
|
|
6325
7170
|
}
|
|
6326
7171
|
}
|
|
6327
|
-
function coordinatePorts(node, nodeBox, portShifting) {
|
|
7172
|
+
function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
|
|
6328
7173
|
const portsBySide = /* @__PURE__ */ new Map();
|
|
6329
7174
|
for (const port of node.ports ?? []) {
|
|
6330
7175
|
const ports = portsBySide.get(port.side) ?? [];
|
|
@@ -6347,7 +7192,9 @@ function coordinatePorts(node, nodeBox, portShifting) {
|
|
|
6347
7192
|
side,
|
|
6348
7193
|
index,
|
|
6349
7194
|
sorted.length,
|
|
6350
|
-
portShifting
|
|
7195
|
+
portShifting,
|
|
7196
|
+
diagnostics,
|
|
7197
|
+
node.id
|
|
6351
7198
|
);
|
|
6352
7199
|
const box = portBox(anchor);
|
|
6353
7200
|
coordinated.push({ ...port, box, anchor });
|
|
@@ -6355,16 +7202,32 @@ function coordinatePorts(node, nodeBox, portShifting) {
|
|
|
6355
7202
|
}
|
|
6356
7203
|
return coordinated.sort((a, b) => a.id.localeCompare(b.id));
|
|
6357
7204
|
}
|
|
6358
|
-
function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
7205
|
+
function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
|
|
6359
7206
|
const shiftingEnabled = portShifting?.enabled ?? true;
|
|
6360
7207
|
const requestedSpacing = portShifting?.spacing ?? 24;
|
|
6361
7208
|
const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
|
|
6362
7209
|
const availableSpan = 2 * maxOffset;
|
|
6363
7210
|
const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
|
|
6364
|
-
const
|
|
7211
|
+
const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
|
|
6365
7212
|
Math.min(requestedSpacing, availableSpan / (count - 1)),
|
|
6366
7213
|
minSpacing
|
|
6367
7214
|
) : requestedSpacing;
|
|
7215
|
+
if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
|
|
7216
|
+
diagnostics.push({
|
|
7217
|
+
severity: "warning",
|
|
7218
|
+
code: "port_constraint_overlap",
|
|
7219
|
+
message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
|
|
7220
|
+
path: ["nodes", nodeId, "ports"],
|
|
7221
|
+
detail: {
|
|
7222
|
+
nodeId,
|
|
7223
|
+
side,
|
|
7224
|
+
requestedSpacing,
|
|
7225
|
+
effectiveSpacing: Math.round(effectiveSpacing),
|
|
7226
|
+
portCount: count
|
|
7227
|
+
}
|
|
7228
|
+
});
|
|
7229
|
+
}
|
|
7230
|
+
const spacing = effectiveSpacing;
|
|
6368
7231
|
const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
|
|
6369
7232
|
switch (side) {
|
|
6370
7233
|
case "left":
|