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