@crazyhappyone/auto-graph 0.2.5 → 0.2.7
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/index.cjs
CHANGED
|
@@ -1037,6 +1037,69 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
});
|
|
1039
1039
|
}
|
|
1040
|
+
if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
|
|
1041
|
+
distributeSwimlaneChildren(input, boxes, locks, diagnostics);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
|
|
1045
|
+
const spread = input.distributeSwimlaneChildren === "spread";
|
|
1046
|
+
const minGap = input.minSiblingGap ?? 8;
|
|
1047
|
+
for (const swimlane of input.swimlanes) {
|
|
1048
|
+
if (swimlane.layout === "contract") continue;
|
|
1049
|
+
const isVertical = swimlane.orientation !== "horizontal";
|
|
1050
|
+
const axis = isVertical ? "x" : "y";
|
|
1051
|
+
const mainSize = isVertical ? "width" : "height";
|
|
1052
|
+
for (const lane of swimlane.lanes) {
|
|
1053
|
+
if (lane.children.length < 2) continue;
|
|
1054
|
+
const unlocked = [];
|
|
1055
|
+
const reserved = [];
|
|
1056
|
+
for (const childId of lane.children) {
|
|
1057
|
+
const box = boxes.get(childId);
|
|
1058
|
+
if (box === void 0) continue;
|
|
1059
|
+
if (locks.has(childId)) {
|
|
1060
|
+
const lock = locks.get(childId);
|
|
1061
|
+
if (lock.source === "fixed-position") {
|
|
1062
|
+
unlocked.push({ id: childId, box });
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
reserved.push(intervalForBox(box, axis, mainSize));
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
unlocked.push({ id: childId, box });
|
|
1069
|
+
}
|
|
1070
|
+
if (unlocked.length < 2) continue;
|
|
1071
|
+
const contentStart = isVertical ? Math.min(...unlocked.map((c) => c.box.x)) : Math.min(...unlocked.map((c) => c.box.y));
|
|
1072
|
+
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));
|
|
1073
|
+
const contentSpan = contentEnd - contentStart;
|
|
1074
|
+
const totalChildSpan = unlocked.reduce((s, c) => s + c.box[mainSize], 0);
|
|
1075
|
+
const reservedSpan = reserved.reduce((s, r) => s + (r.end - r.start), 0);
|
|
1076
|
+
let effectiveGap = minGap;
|
|
1077
|
+
const remaining = contentSpan - totalChildSpan - reservedSpan - minGap * (unlocked.length - 1);
|
|
1078
|
+
if (spread && remaining > 0) {
|
|
1079
|
+
effectiveGap = minGap + remaining / (unlocked.length - 1);
|
|
1080
|
+
}
|
|
1081
|
+
unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
|
|
1082
|
+
let pos = contentStart;
|
|
1083
|
+
for (const child of unlocked) {
|
|
1084
|
+
pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
|
|
1085
|
+
const newBox = { ...child.box };
|
|
1086
|
+
if (axis === "x") {
|
|
1087
|
+
newBox.x = pos;
|
|
1088
|
+
} else {
|
|
1089
|
+
newBox.y = pos;
|
|
1090
|
+
}
|
|
1091
|
+
boxes.set(child.id, newBox);
|
|
1092
|
+
locks.delete(child.id);
|
|
1093
|
+
pos += child.box[mainSize] + effectiveGap;
|
|
1094
|
+
}
|
|
1095
|
+
diagnostics.push({
|
|
1096
|
+
severity: "info",
|
|
1097
|
+
code: "intra_container_distributed",
|
|
1098
|
+
message: `Distributed ${unlocked.length} children in swimlane ${lane.id} along ${axis}.`,
|
|
1099
|
+
detail: { containerId: lane.id, count: unlocked.length, axis }
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1040
1103
|
}
|
|
1041
1104
|
function intervalForBox(box, axis, mainSize) {
|
|
1042
1105
|
return { start: box[axis], end: box[axis] + box[mainSize] };
|
|
@@ -4308,6 +4371,397 @@ function connectedComponents(nodes, edges) {
|
|
|
4308
4371
|
});
|
|
4309
4372
|
}
|
|
4310
4373
|
|
|
4374
|
+
// src/layout/recursive.ts
|
|
4375
|
+
function buildContainerTree(groups, constraints, edges) {
|
|
4376
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4377
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
4378
|
+
for (const group of groups) {
|
|
4379
|
+
const children = [];
|
|
4380
|
+
for (const nodeId of group.nodeIds) {
|
|
4381
|
+
children.push(nodeId);
|
|
4382
|
+
parentOf.set(nodeId, group.id);
|
|
4383
|
+
}
|
|
4384
|
+
for (const childGroupId of group.groupIds) {
|
|
4385
|
+
children.push(childGroupId);
|
|
4386
|
+
parentOf.set(childGroupId, group.id);
|
|
4387
|
+
}
|
|
4388
|
+
childrenOf.set(group.id, children);
|
|
4389
|
+
}
|
|
4390
|
+
for (const c of constraints) {
|
|
4391
|
+
if (c.kind !== "containment") continue;
|
|
4392
|
+
for (const childId of c.childIds) {
|
|
4393
|
+
const existing = parentOf.get(childId);
|
|
4394
|
+
if (existing !== void 0) {
|
|
4395
|
+
if (existing === c.containerId) continue;
|
|
4396
|
+
const oldSiblings = childrenOf.get(existing) ?? [];
|
|
4397
|
+
const pruned = oldSiblings.filter((id) => id !== childId);
|
|
4398
|
+
if (pruned.length === 0) {
|
|
4399
|
+
childrenOf.delete(existing);
|
|
4400
|
+
} else {
|
|
4401
|
+
childrenOf.set(existing, pruned);
|
|
4402
|
+
}
|
|
4403
|
+
const newSiblings = childrenOf.get(c.containerId) ?? [];
|
|
4404
|
+
newSiblings.push(childId);
|
|
4405
|
+
childrenOf.set(c.containerId, newSiblings);
|
|
4406
|
+
parentOf.set(childId, c.containerId);
|
|
4407
|
+
} else {
|
|
4408
|
+
const list = childrenOf.get(c.containerId) ?? [];
|
|
4409
|
+
list.push(childId);
|
|
4410
|
+
childrenOf.set(c.containerId, list);
|
|
4411
|
+
parentOf.set(childId, c.containerId);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
const rootIds = /* @__PURE__ */ new Set();
|
|
4416
|
+
for (const group of groups) {
|
|
4417
|
+
if (!parentOf.has(group.id)) {
|
|
4418
|
+
rootIds.add(group.id);
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
const edgesInGroup = /* @__PURE__ */ new Map();
|
|
4422
|
+
for (const edge of edges) {
|
|
4423
|
+
const srcParent = parentOf.get(edge.source.nodeId);
|
|
4424
|
+
const tgtParent = parentOf.get(edge.target.nodeId);
|
|
4425
|
+
if (srcParent !== void 0 && srcParent === tgtParent) {
|
|
4426
|
+
const list = edgesInGroup.get(srcParent) ?? [];
|
|
4427
|
+
list.push(edge);
|
|
4428
|
+
edgesInGroup.set(srcParent, list);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
const treeDiagnostics = [];
|
|
4432
|
+
return { childrenOf, rootIds, edgesInGroup, diagnostics: treeDiagnostics };
|
|
4433
|
+
}
|
|
4434
|
+
function runRecursiveContainerLayout(input) {
|
|
4435
|
+
const diagnostics = [];
|
|
4436
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
4437
|
+
const groupBoxes = /* @__PURE__ */ new Map();
|
|
4438
|
+
const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
|
|
4439
|
+
const groupById = new Map(input.groups.map((g) => [g.id, g]));
|
|
4440
|
+
const groupIdSet = new Set(input.groups.map((g) => g.id));
|
|
4441
|
+
const {
|
|
4442
|
+
childrenOf,
|
|
4443
|
+
rootIds,
|
|
4444
|
+
edgesInGroup,
|
|
4445
|
+
diagnostics: treeDiagnostics
|
|
4446
|
+
} = buildContainerTree(input.groups, input.constraints, input.edges);
|
|
4447
|
+
diagnostics.push(...treeDiagnostics);
|
|
4448
|
+
if (input.groups.length === 0) {
|
|
4449
|
+
const flat = runComponentAwareDagreInitialLayout({
|
|
4450
|
+
direction: input.direction,
|
|
4451
|
+
nodes: input.nodes.map((n) => ({ id: n.id, size: n.size })),
|
|
4452
|
+
edges: input.edges.map((e) => ({
|
|
4453
|
+
id: e.id,
|
|
4454
|
+
sourceId: e.source.nodeId,
|
|
4455
|
+
targetId: e.target.nodeId
|
|
4456
|
+
})),
|
|
4457
|
+
...input.options === void 0 ? {} : { options: input.options }
|
|
4458
|
+
});
|
|
4459
|
+
diagnostics.push(...flat.diagnostics);
|
|
4460
|
+
for (const [id, box] of flat.boxes) {
|
|
4461
|
+
boxes.set(id, box);
|
|
4462
|
+
}
|
|
4463
|
+
return { boxes, groupBoxes, diagnostics };
|
|
4464
|
+
}
|
|
4465
|
+
const descendants = /* @__PURE__ */ new Map();
|
|
4466
|
+
function collectDescendants(groupId) {
|
|
4467
|
+
const cached = descendants.get(groupId);
|
|
4468
|
+
if (cached !== void 0) return cached;
|
|
4469
|
+
const result = /* @__PURE__ */ new Set();
|
|
4470
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
4471
|
+
for (const childId of children) {
|
|
4472
|
+
result.add(childId);
|
|
4473
|
+
const childDesc = collectDescendants(childId);
|
|
4474
|
+
for (const d of childDesc) result.add(d);
|
|
4475
|
+
}
|
|
4476
|
+
descendants.set(groupId, result);
|
|
4477
|
+
return result;
|
|
4478
|
+
}
|
|
4479
|
+
const groupOrder = topologicalSort(input.groups, childrenOf, groupIdSet);
|
|
4480
|
+
for (const groupId of groupOrder) {
|
|
4481
|
+
const group = groupById.get(groupId);
|
|
4482
|
+
if (group === void 0) continue;
|
|
4483
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
4484
|
+
if (children.length === 0) {
|
|
4485
|
+
const box = {
|
|
4486
|
+
x: 0,
|
|
4487
|
+
y: 0,
|
|
4488
|
+
width: (group.padding?.left ?? 8) + (group.padding?.right ?? 8) + 40,
|
|
4489
|
+
height: (group.padding?.top ?? 8) + (group.padding?.bottom ?? 8) + 20
|
|
4490
|
+
};
|
|
4491
|
+
groupBoxes.set(groupId, box);
|
|
4492
|
+
boxes.set(groupId, box);
|
|
4493
|
+
continue;
|
|
4494
|
+
}
|
|
4495
|
+
const leafNodeIds = [];
|
|
4496
|
+
const nestedGroupIds = [];
|
|
4497
|
+
for (const childId of children) {
|
|
4498
|
+
if (groupIdSet.has(childId)) {
|
|
4499
|
+
nestedGroupIds.push(childId);
|
|
4500
|
+
} else {
|
|
4501
|
+
leafNodeIds.push(childId);
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
const childSizes = /* @__PURE__ */ new Map();
|
|
4505
|
+
for (const nodeId of leafNodeIds) {
|
|
4506
|
+
const node = nodeById.get(nodeId);
|
|
4507
|
+
if (node !== void 0) {
|
|
4508
|
+
childSizes.set(nodeId, {
|
|
4509
|
+
width: node.size.width,
|
|
4510
|
+
height: node.size.height
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
for (const nestedId of nestedGroupIds) {
|
|
4515
|
+
const nestedBox = groupBoxes.get(nestedId);
|
|
4516
|
+
if (nestedBox !== void 0) {
|
|
4517
|
+
childSizes.set(nestedId, {
|
|
4518
|
+
width: nestedBox.width,
|
|
4519
|
+
height: nestedBox.height
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
const groupEdges = edgesInGroup.get(groupId) ?? [];
|
|
4524
|
+
const childLayout = runDagreInitialLayout({
|
|
4525
|
+
direction: input.direction,
|
|
4526
|
+
nodes: children.flatMap((childId) => {
|
|
4527
|
+
const size = childSizes.get(childId);
|
|
4528
|
+
return size === void 0 ? [] : [{ id: childId, size }];
|
|
4529
|
+
}),
|
|
4530
|
+
edges: groupEdges.map((e) => ({
|
|
4531
|
+
id: e.id,
|
|
4532
|
+
sourceId: e.source.nodeId,
|
|
4533
|
+
targetId: e.target.nodeId
|
|
4534
|
+
})),
|
|
4535
|
+
options: {
|
|
4536
|
+
...input.options ?? {},
|
|
4537
|
+
ranksep: (input.options?.ranksep ?? 100) * 0.6,
|
|
4538
|
+
// tighter inside containers
|
|
4539
|
+
nodesep: (input.options?.nodesep ?? 80) * 0.6
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4542
|
+
diagnostics.push(...childLayout.diagnostics);
|
|
4543
|
+
if (childLayout.boxes.size === 0) continue;
|
|
4544
|
+
const childBoxes = [...childLayout.boxes.values()];
|
|
4545
|
+
const contentBounds = unionBoxes(childBoxes);
|
|
4546
|
+
const padding = group.padding ?? {
|
|
4547
|
+
top: 8,
|
|
4548
|
+
right: 8,
|
|
4549
|
+
bottom: 8,
|
|
4550
|
+
left: 8
|
|
4551
|
+
};
|
|
4552
|
+
const containerBox = {
|
|
4553
|
+
x: 0,
|
|
4554
|
+
y: 0,
|
|
4555
|
+
width: contentBounds.width + (padding.left ?? 8) + (padding.right ?? 8),
|
|
4556
|
+
height: contentBounds.height + (padding.top ?? 8) + (padding.bottom ?? 8)
|
|
4557
|
+
};
|
|
4558
|
+
const offsetX = padding.left ?? 8;
|
|
4559
|
+
const offsetY = padding.top ?? 8;
|
|
4560
|
+
for (const [childId, childBox] of childLayout.boxes) {
|
|
4561
|
+
boxes.set(childId, {
|
|
4562
|
+
...childBox,
|
|
4563
|
+
x: childBox.x + offsetX,
|
|
4564
|
+
y: childBox.y + offsetY
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4567
|
+
groupBoxes.set(groupId, containerBox);
|
|
4568
|
+
boxes.set(groupId, containerBox);
|
|
4569
|
+
}
|
|
4570
|
+
const allContainedIds = /* @__PURE__ */ new Set();
|
|
4571
|
+
for (const [, childIds] of childrenOf) {
|
|
4572
|
+
for (const cid of childIds) allContainedIds.add(cid);
|
|
4573
|
+
}
|
|
4574
|
+
const topLevelNodeIds = /* @__PURE__ */ new Set();
|
|
4575
|
+
for (const node of input.nodes) {
|
|
4576
|
+
if (!allContainedIds.has(node.id)) {
|
|
4577
|
+
topLevelNodeIds.add(node.id);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
const globalNodes = [];
|
|
4581
|
+
for (const nodeId of topLevelNodeIds) {
|
|
4582
|
+
const node = nodeById.get(nodeId);
|
|
4583
|
+
if (node !== void 0) {
|
|
4584
|
+
globalNodes.push({ id: nodeId, size: node.size });
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
for (const rootId of rootIds) {
|
|
4588
|
+
const gb = groupBoxes.get(rootId);
|
|
4589
|
+
if (gb !== void 0) {
|
|
4590
|
+
globalNodes.push({
|
|
4591
|
+
id: rootId,
|
|
4592
|
+
size: { width: gb.width, height: gb.height }
|
|
4593
|
+
});
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
function rootContainerOf(id) {
|
|
4597
|
+
for (const rootId of rootIds) {
|
|
4598
|
+
const desc = collectDescendants(rootId);
|
|
4599
|
+
if (desc.has(id)) return rootId;
|
|
4600
|
+
}
|
|
4601
|
+
return void 0;
|
|
4602
|
+
}
|
|
4603
|
+
const globalEdges = input.edges.filter((e) => {
|
|
4604
|
+
for (const group of input.groups) {
|
|
4605
|
+
const desc = collectDescendants(group.id);
|
|
4606
|
+
if (desc.has(e.source.nodeId) && desc.has(e.target.nodeId)) {
|
|
4607
|
+
return false;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
return true;
|
|
4611
|
+
}).map((e) => ({
|
|
4612
|
+
id: e.id,
|
|
4613
|
+
sourceId: rootContainerOf(e.source.nodeId) ?? e.source.nodeId,
|
|
4614
|
+
targetId: rootContainerOf(e.target.nodeId) ?? e.target.nodeId
|
|
4615
|
+
}));
|
|
4616
|
+
if (globalNodes.length > 0) {
|
|
4617
|
+
const globalLayout = runDagreInitialLayout({
|
|
4618
|
+
direction: input.direction,
|
|
4619
|
+
nodes: globalNodes,
|
|
4620
|
+
edges: globalEdges,
|
|
4621
|
+
...input.options === void 0 ? {} : { options: input.options }
|
|
4622
|
+
});
|
|
4623
|
+
diagnostics.push(...globalLayout.diagnostics);
|
|
4624
|
+
for (const [id, box] of globalLayout.boxes) {
|
|
4625
|
+
if (groupBoxes.has(id)) {
|
|
4626
|
+
groupBoxes.set(id, box);
|
|
4627
|
+
boxes.set(id, box);
|
|
4628
|
+
} else if (topLevelNodeIds.has(id)) {
|
|
4629
|
+
boxes.set(id, box);
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
for (const groupId of groupOrder) {
|
|
4633
|
+
const containerBox = groupBoxes.get(groupId);
|
|
4634
|
+
if (containerBox === void 0) continue;
|
|
4635
|
+
const offsetX = containerBox.x;
|
|
4636
|
+
const offsetY = containerBox.y;
|
|
4637
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
4638
|
+
for (const childId of children) {
|
|
4639
|
+
const childBox = boxes.get(childId);
|
|
4640
|
+
if (childBox !== void 0) {
|
|
4641
|
+
boxes.set(childId, {
|
|
4642
|
+
...childBox,
|
|
4643
|
+
x: childBox.x + offsetX,
|
|
4644
|
+
y: childBox.y + offsetY
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
translateDescendants(childId, offsetX, offsetY, boxes, childrenOf);
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
return { boxes, groupBoxes, diagnostics };
|
|
4652
|
+
}
|
|
4653
|
+
function translateDescendants(groupId, dx, dy, boxes, childrenOf) {
|
|
4654
|
+
const children = childrenOf.get(groupId) ?? [];
|
|
4655
|
+
for (const childId of children) {
|
|
4656
|
+
const box = boxes.get(childId);
|
|
4657
|
+
if (box !== void 0) {
|
|
4658
|
+
boxes.set(childId, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4659
|
+
}
|
|
4660
|
+
translateDescendants(childId, dx, dy, boxes, childrenOf);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
function topologicalSort(groups, childrenOf, groupIdSet) {
|
|
4664
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4665
|
+
const result = [];
|
|
4666
|
+
function visit(id) {
|
|
4667
|
+
if (visited.has(id)) return;
|
|
4668
|
+
visited.add(id);
|
|
4669
|
+
const children = childrenOf.get(id) ?? [];
|
|
4670
|
+
for (const childId of children) {
|
|
4671
|
+
if (groupIdSet.has(childId)) {
|
|
4672
|
+
visit(childId);
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
result.push(id);
|
|
4676
|
+
}
|
|
4677
|
+
for (const group of groups) {
|
|
4678
|
+
visit(group.id);
|
|
4679
|
+
}
|
|
4680
|
+
return result;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
// src/routing/binary-heap.ts
|
|
4684
|
+
var BinaryHeap = class {
|
|
4685
|
+
_data = [];
|
|
4686
|
+
_nextOrder = 0;
|
|
4687
|
+
get size() {
|
|
4688
|
+
return this._data.length;
|
|
4689
|
+
}
|
|
4690
|
+
push(value, priority) {
|
|
4691
|
+
const entry = { value, priority, order: this._nextOrder++ };
|
|
4692
|
+
this._data.push(entry);
|
|
4693
|
+
this._siftUp(this._data.length - 1);
|
|
4694
|
+
}
|
|
4695
|
+
pop() {
|
|
4696
|
+
if (this._data.length === 0) return void 0;
|
|
4697
|
+
const top = this._data[0];
|
|
4698
|
+
const last = this._data.pop();
|
|
4699
|
+
if (this._data.length > 0) {
|
|
4700
|
+
this._data[0] = last;
|
|
4701
|
+
this._siftDown(0);
|
|
4702
|
+
}
|
|
4703
|
+
return top.value;
|
|
4704
|
+
}
|
|
4705
|
+
peek() {
|
|
4706
|
+
return this._data.length > 0 ? this._data[0].value : void 0;
|
|
4707
|
+
}
|
|
4708
|
+
// -----------------------------------------------------------------------
|
|
4709
|
+
// Internals
|
|
4710
|
+
// -----------------------------------------------------------------------
|
|
4711
|
+
_siftUp(idx) {
|
|
4712
|
+
const entry = this._data[idx];
|
|
4713
|
+
while (idx > 0) {
|
|
4714
|
+
const parentIdx = idx - 1 >> 1;
|
|
4715
|
+
const parent = this._data[parentIdx];
|
|
4716
|
+
if (this._less(entry, parent)) {
|
|
4717
|
+
this._data[idx] = parent;
|
|
4718
|
+
idx = parentIdx;
|
|
4719
|
+
} else {
|
|
4720
|
+
break;
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
this._data[idx] = entry;
|
|
4724
|
+
}
|
|
4725
|
+
_siftDown(idx) {
|
|
4726
|
+
const entry = this._data[idx];
|
|
4727
|
+
const size = this._data.length;
|
|
4728
|
+
while (true) {
|
|
4729
|
+
let smallestIdx = idx;
|
|
4730
|
+
const leftIdx = (idx << 1) + 1;
|
|
4731
|
+
const rightIdx = leftIdx + 1;
|
|
4732
|
+
if (leftIdx < size && this._less(
|
|
4733
|
+
this._data[leftIdx],
|
|
4734
|
+
this._data[smallestIdx]
|
|
4735
|
+
)) {
|
|
4736
|
+
smallestIdx = leftIdx;
|
|
4737
|
+
}
|
|
4738
|
+
if (rightIdx < size && this._less(
|
|
4739
|
+
this._data[rightIdx],
|
|
4740
|
+
this._data[smallestIdx]
|
|
4741
|
+
)) {
|
|
4742
|
+
smallestIdx = rightIdx;
|
|
4743
|
+
}
|
|
4744
|
+
if (smallestIdx !== idx) {
|
|
4745
|
+
this._data[idx] = this._data[smallestIdx];
|
|
4746
|
+
idx = smallestIdx;
|
|
4747
|
+
} else {
|
|
4748
|
+
break;
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
this._data[idx] = entry;
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* Two entries are compared first by priority, then by insertion order
|
|
4755
|
+
* when priorities are equal. The insertion-order tie-break makes the
|
|
4756
|
+
* heap deterministic: for a given sequence of {value, priority} pushes,
|
|
4757
|
+
* the extraction order is always the same.
|
|
4758
|
+
*/
|
|
4759
|
+
_less(a, b) {
|
|
4760
|
+
if (a.priority !== b.priority) return a.priority < b.priority;
|
|
4761
|
+
return a.order < b.order;
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
|
|
4311
4765
|
// src/routing/astar.ts
|
|
4312
4766
|
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
4313
4767
|
const margin = options.margin ?? 0;
|
|
@@ -4315,8 +4769,17 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
4315
4769
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
4316
4770
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
4317
4771
|
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
4318
|
-
const
|
|
4319
|
-
const
|
|
4772
|
+
const useCorridor = options.corridorPrefilter ?? true;
|
|
4773
|
+
const corridorMargin = options.corridorMargin ?? 32;
|
|
4774
|
+
const filtered = useCorridor ? filterObstaclesByCorridor(
|
|
4775
|
+
source,
|
|
4776
|
+
target,
|
|
4777
|
+
obstacles,
|
|
4778
|
+
endpointObstacles,
|
|
4779
|
+
corridorMargin
|
|
4780
|
+
) : obstacles;
|
|
4781
|
+
const xs = collectXs(source, target, filtered, margin);
|
|
4782
|
+
const ys = collectYs(source, target, filtered, margin);
|
|
4320
4783
|
if (xs.length * ys.length > maxNodes) {
|
|
4321
4784
|
diagnostics?.push({
|
|
4322
4785
|
severity: "warning",
|
|
@@ -4331,8 +4794,8 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
4331
4794
|
return null;
|
|
4332
4795
|
}
|
|
4333
4796
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
4334
|
-
connectHorizontalEdges(nodes, ys,
|
|
4335
|
-
connectVerticalEdges(nodes, xs,
|
|
4797
|
+
connectHorizontalEdges(nodes, ys, filtered, endpointObstacles, margin);
|
|
4798
|
+
connectVerticalEdges(nodes, xs, filtered, endpointObstacles, margin);
|
|
4336
4799
|
const path = aStarSearch(
|
|
4337
4800
|
nodes,
|
|
4338
4801
|
nodeIndex,
|
|
@@ -4344,6 +4807,22 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
|
|
|
4344
4807
|
if (path === null) return null;
|
|
4345
4808
|
return simplifyRoute(path);
|
|
4346
4809
|
}
|
|
4810
|
+
function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
|
|
4811
|
+
const cx1 = Math.min(source.x, target.x) - margin;
|
|
4812
|
+
const cx2 = Math.max(source.x, target.x) + margin;
|
|
4813
|
+
const cy1 = Math.min(source.y, target.y) - margin;
|
|
4814
|
+
const cy2 = Math.max(source.y, target.y) + margin;
|
|
4815
|
+
const result = [];
|
|
4816
|
+
for (const obs of obstacles) {
|
|
4817
|
+
if (obs.x + obs.width >= cx1 && obs.x <= cx2 && obs.y + obs.height >= cy1 && obs.y <= cy2) {
|
|
4818
|
+
result.push(obs);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
for (const ep of endpointObstacles) {
|
|
4822
|
+
result.push(ep);
|
|
4823
|
+
}
|
|
4824
|
+
return result;
|
|
4825
|
+
}
|
|
4347
4826
|
function collectXs(source, target, obstacles, margin) {
|
|
4348
4827
|
const raw = [];
|
|
4349
4828
|
for (const obs of obstacles) {
|
|
@@ -4487,25 +4966,17 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
|
|
|
4487
4966
|
gScore.set(startId, 0);
|
|
4488
4967
|
const cameFrom = /* @__PURE__ */ new Map();
|
|
4489
4968
|
const cameFromDir = /* @__PURE__ */ new Map();
|
|
4490
|
-
const openSet =
|
|
4491
|
-
openSet.push(
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
for (let i = 1; i < openSet.length; i++) {
|
|
4498
|
-
if (openSet[i].f < openSet[bestIdx].f) {
|
|
4499
|
-
bestIdx = i;
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
|
-
const current = openSet.splice(bestIdx, 1)[0];
|
|
4503
|
-
if (current.id === goalId) {
|
|
4969
|
+
const openSet = new BinaryHeap();
|
|
4970
|
+
openSet.push(startId, manhattan(source, target));
|
|
4971
|
+
while (openSet.size > 0) {
|
|
4972
|
+
const currentId = openSet.pop();
|
|
4973
|
+
const currentG = gScore.get(currentId);
|
|
4974
|
+
if (currentG === void 0) continue;
|
|
4975
|
+
if (currentId === goalId) {
|
|
4504
4976
|
return reconstructPath(nodes, cameFrom, goalId);
|
|
4505
4977
|
}
|
|
4506
|
-
const node = nodes[
|
|
4507
|
-
const
|
|
4508
|
-
const prevDir = cameFromDir.get(current.id);
|
|
4978
|
+
const node = nodes[currentId];
|
|
4979
|
+
const prevDir = cameFromDir.get(currentId);
|
|
4509
4980
|
for (const [neighborId, edgeCost] of node.neighbors) {
|
|
4510
4981
|
const neighbor = nodes[neighborId];
|
|
4511
4982
|
const tentativeG = currentG + edgeCost * segmentPenalty;
|
|
@@ -4515,10 +4986,10 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
|
|
|
4515
4986
|
const existingG = gScore.get(neighborId);
|
|
4516
4987
|
if (existingG === void 0 || totalG < existingG) {
|
|
4517
4988
|
gScore.set(neighborId, totalG);
|
|
4518
|
-
cameFrom.set(neighborId,
|
|
4989
|
+
cameFrom.set(neighborId, currentId);
|
|
4519
4990
|
cameFromDir.set(neighborId, newDir);
|
|
4520
4991
|
const f = totalG + manhattan(neighbor, target);
|
|
4521
|
-
openSet.push(
|
|
4992
|
+
openSet.push(neighborId, f);
|
|
4522
4993
|
}
|
|
4523
4994
|
}
|
|
4524
4995
|
}
|
|
@@ -4555,6 +5026,292 @@ function areCollinear(a, b, c) {
|
|
|
4555
5026
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
4556
5027
|
}
|
|
4557
5028
|
|
|
5029
|
+
// src/routing/visibility-router.ts
|
|
5030
|
+
function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
|
|
5031
|
+
const margin = options.margin ?? 0;
|
|
5032
|
+
const turnPenalty = options.turnPenalty ?? 50;
|
|
5033
|
+
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
5034
|
+
const endpointObstacles = options.endpointObstacles ?? [];
|
|
5035
|
+
const maxCorners = options.maxCorners ?? 300;
|
|
5036
|
+
const vertices = collectCornerVertices(source, target, obstacles, margin);
|
|
5037
|
+
if (vertices.length > maxCorners) {
|
|
5038
|
+
diagnostics?.push({
|
|
5039
|
+
severity: "warning",
|
|
5040
|
+
code: "routing.visibility.corner_overflow",
|
|
5041
|
+
message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
|
|
5042
|
+
detail: { vertexCount: vertices.length, maxCorners }
|
|
5043
|
+
});
|
|
5044
|
+
return null;
|
|
5045
|
+
}
|
|
5046
|
+
if (obstacles.length === 0) {
|
|
5047
|
+
return simplifyRoute2([source, target]);
|
|
5048
|
+
}
|
|
5049
|
+
const expandedObstacles = margin === 0 ? obstacles : obstacles.map((o) => expandBox2(o, margin));
|
|
5050
|
+
const edges = buildVisibilityEdges(
|
|
5051
|
+
vertices,
|
|
5052
|
+
expandedObstacles,
|
|
5053
|
+
endpointObstacles
|
|
5054
|
+
);
|
|
5055
|
+
const path = aStarSearch2(
|
|
5056
|
+
vertices,
|
|
5057
|
+
edges,
|
|
5058
|
+
source,
|
|
5059
|
+
target,
|
|
5060
|
+
segmentPenalty,
|
|
5061
|
+
turnPenalty
|
|
5062
|
+
);
|
|
5063
|
+
if (path === null) return null;
|
|
5064
|
+
return simplifyRoute2(path, expandedObstacles);
|
|
5065
|
+
}
|
|
5066
|
+
function collectCornerVertices(source, target, obstacles, margin = 0) {
|
|
5067
|
+
const vertices = [
|
|
5068
|
+
{ point: { x: source.x, y: source.y }, obstacleIndex: -1 },
|
|
5069
|
+
{ point: { x: target.x, y: target.y }, obstacleIndex: -1 }
|
|
5070
|
+
];
|
|
5071
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5072
|
+
seen.add(`${source.x},${source.y}`);
|
|
5073
|
+
seen.add(`${target.x},${target.y}`);
|
|
5074
|
+
const addVertex = (p, obstacleIndex) => {
|
|
5075
|
+
const key = `${p.x},${p.y}`;
|
|
5076
|
+
if (seen.has(key)) return;
|
|
5077
|
+
seen.add(key);
|
|
5078
|
+
vertices.push({ point: p, obstacleIndex });
|
|
5079
|
+
};
|
|
5080
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
5081
|
+
const obs = obstacles[i];
|
|
5082
|
+
const c0 = { x: obs.x - margin, y: obs.y - margin };
|
|
5083
|
+
const c1 = { x: obs.x + obs.width + margin, y: obs.y - margin };
|
|
5084
|
+
const c2 = {
|
|
5085
|
+
x: obs.x + obs.width + margin,
|
|
5086
|
+
y: obs.y + obs.height + margin
|
|
5087
|
+
};
|
|
5088
|
+
const c3 = { x: obs.x - margin, y: obs.y + obs.height + margin };
|
|
5089
|
+
for (const c of [c0, c1, c2, c3]) {
|
|
5090
|
+
addVertex(c, i);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
for (const p of [source, target]) {
|
|
5094
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
5095
|
+
const obs = obstacles[i];
|
|
5096
|
+
addVertex({ x: obs.x - margin, y: p.y }, i);
|
|
5097
|
+
addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
|
|
5098
|
+
addVertex({ x: p.x, y: obs.y - margin }, i);
|
|
5099
|
+
addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
|
|
5100
|
+
if (p.x > obs.x && p.x < obs.x + obs.width) {
|
|
5101
|
+
addVertex({ x: p.x, y: obs.y - margin }, i);
|
|
5102
|
+
addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
|
|
5103
|
+
}
|
|
5104
|
+
if (p.y > obs.y && p.y < obs.y + obs.height) {
|
|
5105
|
+
addVertex({ x: obs.x - margin, y: p.y }, i);
|
|
5106
|
+
addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
return vertices;
|
|
5111
|
+
}
|
|
5112
|
+
function buildVisibilityEdges(vertices, obstacles, endpointObstacles) {
|
|
5113
|
+
const edges = [];
|
|
5114
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
5115
|
+
const v = vertices[i];
|
|
5116
|
+
const right = visibleInDirection(
|
|
5117
|
+
v,
|
|
5118
|
+
vertices,
|
|
5119
|
+
obstacles,
|
|
5120
|
+
endpointObstacles,
|
|
5121
|
+
"right"
|
|
5122
|
+
);
|
|
5123
|
+
const left = visibleInDirection(
|
|
5124
|
+
v,
|
|
5125
|
+
vertices,
|
|
5126
|
+
obstacles,
|
|
5127
|
+
endpointObstacles,
|
|
5128
|
+
"left"
|
|
5129
|
+
);
|
|
5130
|
+
const down = visibleInDirection(
|
|
5131
|
+
v,
|
|
5132
|
+
vertices,
|
|
5133
|
+
obstacles,
|
|
5134
|
+
endpointObstacles,
|
|
5135
|
+
"down"
|
|
5136
|
+
);
|
|
5137
|
+
const up = visibleInDirection(
|
|
5138
|
+
v,
|
|
5139
|
+
vertices,
|
|
5140
|
+
obstacles,
|
|
5141
|
+
endpointObstacles,
|
|
5142
|
+
"up"
|
|
5143
|
+
);
|
|
5144
|
+
for (const neighbor of [right, left, down, up]) {
|
|
5145
|
+
if (neighbor !== null && neighbor.index > i) {
|
|
5146
|
+
edges.push({
|
|
5147
|
+
from: i,
|
|
5148
|
+
to: neighbor.index,
|
|
5149
|
+
cost: neighbor.distance
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
return edges;
|
|
5155
|
+
}
|
|
5156
|
+
function visibleInDirection(origin, vertices, obstacles, endpointObstacles, dir) {
|
|
5157
|
+
const candidates = [];
|
|
5158
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
5159
|
+
const v = vertices[i];
|
|
5160
|
+
const dx = v.point.x - origin.point.x;
|
|
5161
|
+
const dy = v.point.y - origin.point.y;
|
|
5162
|
+
switch (dir) {
|
|
5163
|
+
case "right":
|
|
5164
|
+
if (dx > 0 && dy === 0) candidates.push({ index: i, dist: dx });
|
|
5165
|
+
break;
|
|
5166
|
+
case "left":
|
|
5167
|
+
if (dx < 0 && dy === 0) candidates.push({ index: i, dist: -dx });
|
|
5168
|
+
break;
|
|
5169
|
+
case "down":
|
|
5170
|
+
if (dy > 0 && dx === 0) candidates.push({ index: i, dist: dy });
|
|
5171
|
+
break;
|
|
5172
|
+
case "up":
|
|
5173
|
+
if (dy < 0 && dx === 0) candidates.push({ index: i, dist: -dy });
|
|
5174
|
+
break;
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
candidates.sort((a, b) => a.dist - b.dist);
|
|
5178
|
+
for (const c of candidates) {
|
|
5179
|
+
if (isSegmentVisible(
|
|
5180
|
+
origin.point,
|
|
5181
|
+
vertices[c.index].point,
|
|
5182
|
+
obstacles,
|
|
5183
|
+
endpointObstacles,
|
|
5184
|
+
origin.obstacleIndex,
|
|
5185
|
+
vertices[c.index].obstacleIndex
|
|
5186
|
+
)) {
|
|
5187
|
+
return { index: c.index, distance: c.dist };
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
return null;
|
|
5191
|
+
}
|
|
5192
|
+
function isSegmentVisible(a, b, obstacles, endpointObstacles, _aObsIdx, _bObsIdx) {
|
|
5193
|
+
for (let i = 0; i < obstacles.length; i++) {
|
|
5194
|
+
if (segmentEntersBox(a, b, obstacles[i])) return false;
|
|
5195
|
+
}
|
|
5196
|
+
for (const ep of endpointObstacles) {
|
|
5197
|
+
if (segmentEntersBox(a, b, ep)) return false;
|
|
5198
|
+
}
|
|
5199
|
+
return true;
|
|
5200
|
+
}
|
|
5201
|
+
function segmentEntersBox(start, end, box) {
|
|
5202
|
+
const left = box.x;
|
|
5203
|
+
const right = box.x + box.width;
|
|
5204
|
+
const top = box.y;
|
|
5205
|
+
const bottom = box.y + box.height;
|
|
5206
|
+
const inside = (p) => p.x > left && p.x < right && p.y > top && p.y < bottom;
|
|
5207
|
+
if (inside(start) || inside(end)) return true;
|
|
5208
|
+
if (start.x === end.x) {
|
|
5209
|
+
return start.x > left && start.x < right && Math.max(start.y, end.y) > top && Math.min(start.y, end.y) < bottom;
|
|
5210
|
+
}
|
|
5211
|
+
if (start.y === end.y) {
|
|
5212
|
+
return start.y > top && start.y < bottom && Math.max(start.x, end.x) > left && Math.min(start.x, end.x) < right;
|
|
5213
|
+
}
|
|
5214
|
+
return false;
|
|
5215
|
+
}
|
|
5216
|
+
function expandBox2(box, margin) {
|
|
5217
|
+
return {
|
|
5218
|
+
x: box.x - margin,
|
|
5219
|
+
y: box.y - margin,
|
|
5220
|
+
width: box.width + margin * 2,
|
|
5221
|
+
height: box.height + margin * 2
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
function aStarSearch2(vertices, edges, source, target, segmentPenalty, turnPenalty) {
|
|
5225
|
+
const startId = 0;
|
|
5226
|
+
const goalId = 1;
|
|
5227
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
5228
|
+
gScore.set(startId, 0);
|
|
5229
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
5230
|
+
const cameFromDir = /* @__PURE__ */ new Map();
|
|
5231
|
+
const openSet = new BinaryHeap();
|
|
5232
|
+
openSet.push(startId, manhattan2(source, target));
|
|
5233
|
+
const neighborMap = /* @__PURE__ */ new Map();
|
|
5234
|
+
for (const e of edges) {
|
|
5235
|
+
let list = neighborMap.get(e.from);
|
|
5236
|
+
if (list === void 0) {
|
|
5237
|
+
list = [];
|
|
5238
|
+
neighborMap.set(e.from, list);
|
|
5239
|
+
}
|
|
5240
|
+
list.push({ to: e.to, cost: e.cost });
|
|
5241
|
+
list = neighborMap.get(e.to);
|
|
5242
|
+
if (list === void 0) {
|
|
5243
|
+
list = [];
|
|
5244
|
+
neighborMap.set(e.to, list);
|
|
5245
|
+
}
|
|
5246
|
+
list.push({ to: e.from, cost: e.cost });
|
|
5247
|
+
}
|
|
5248
|
+
while (openSet.size > 0) {
|
|
5249
|
+
const currentId = openSet.pop();
|
|
5250
|
+
const currentG = gScore.get(currentId);
|
|
5251
|
+
if (currentG === void 0) continue;
|
|
5252
|
+
if (currentId === goalId) {
|
|
5253
|
+
return reconstructPath2(vertices, cameFrom, goalId);
|
|
5254
|
+
}
|
|
5255
|
+
const prevDir = cameFromDir.get(currentId);
|
|
5256
|
+
const neighbors = neighborMap.get(currentId) ?? [];
|
|
5257
|
+
for (const { to, cost } of neighbors) {
|
|
5258
|
+
const tentativeG = currentG + cost * segmentPenalty;
|
|
5259
|
+
const toV = vertices[to];
|
|
5260
|
+
const curV = vertices[currentId];
|
|
5261
|
+
const newDir = toV.point.y === curV.point.y ? "h" : "v";
|
|
5262
|
+
const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
|
|
5263
|
+
const totalG = tentativeG + turnCost;
|
|
5264
|
+
const existingG = gScore.get(to);
|
|
5265
|
+
if (existingG === void 0 || totalG < existingG) {
|
|
5266
|
+
gScore.set(to, totalG);
|
|
5267
|
+
cameFrom.set(to, currentId);
|
|
5268
|
+
cameFromDir.set(to, newDir);
|
|
5269
|
+
const f = totalG + manhattan2(toV.point, target);
|
|
5270
|
+
openSet.push(to, f);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
return null;
|
|
5275
|
+
}
|
|
5276
|
+
function manhattan2(a, b) {
|
|
5277
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
5278
|
+
}
|
|
5279
|
+
function reconstructPath2(vertices, cameFrom, goalId) {
|
|
5280
|
+
const path = [];
|
|
5281
|
+
let current = goalId;
|
|
5282
|
+
while (current !== void 0) {
|
|
5283
|
+
const v = vertices[current];
|
|
5284
|
+
path.unshift({ x: v.point.x, y: v.point.y });
|
|
5285
|
+
current = cameFrom.get(current);
|
|
5286
|
+
}
|
|
5287
|
+
return path;
|
|
5288
|
+
}
|
|
5289
|
+
function simplifyRoute2(points, obstacles = []) {
|
|
5290
|
+
if (points.length <= 2) return [...points];
|
|
5291
|
+
const result = [points[0]];
|
|
5292
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
5293
|
+
const prev = result[result.length - 1];
|
|
5294
|
+
const curr = points[i];
|
|
5295
|
+
const next = points[i + 1];
|
|
5296
|
+
const collinear = prev.x === curr.x && curr.x === next.x || prev.y === curr.y && curr.y === next.y;
|
|
5297
|
+
if (!collinear) {
|
|
5298
|
+
result.push(curr);
|
|
5299
|
+
continue;
|
|
5300
|
+
}
|
|
5301
|
+
if (segmentCrossesAnyObstacle(prev, next, obstacles)) {
|
|
5302
|
+
result.push(curr);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
result.push(points[points.length - 1]);
|
|
5306
|
+
return result;
|
|
5307
|
+
}
|
|
5308
|
+
function segmentCrossesAnyObstacle(a, b, obstacles) {
|
|
5309
|
+
for (const obs of obstacles) {
|
|
5310
|
+
if (segmentEntersBox(a, b, obs)) return true;
|
|
5311
|
+
}
|
|
5312
|
+
return false;
|
|
5313
|
+
}
|
|
5314
|
+
|
|
4558
5315
|
// src/routing/routes.ts
|
|
4559
5316
|
function checkBacktracking(points, source, target, diagnostics) {
|
|
4560
5317
|
if (points.length < 2) return;
|
|
@@ -4644,13 +5401,18 @@ function routeEdge(input) {
|
|
|
4644
5401
|
input.source.center,
|
|
4645
5402
|
targetAnchor
|
|
4646
5403
|
);
|
|
4647
|
-
const
|
|
5404
|
+
const cornerPath = findCornerGraphPath(
|
|
4648
5405
|
source,
|
|
4649
5406
|
target,
|
|
4650
5407
|
[...softObstacles, ...hardObstacles],
|
|
4651
|
-
{
|
|
4652
|
-
|
|
4653
|
-
|
|
5408
|
+
{ endpointObstacles, margin: 2 },
|
|
5409
|
+
diagnostics
|
|
5410
|
+
);
|
|
5411
|
+
const path = cornerPath ?? findObstacleFreePath(
|
|
5412
|
+
source,
|
|
5413
|
+
target,
|
|
5414
|
+
[...softObstacles, ...hardObstacles],
|
|
5415
|
+
{ endpointObstacles, margin: 0 },
|
|
4654
5416
|
diagnostics
|
|
4655
5417
|
);
|
|
4656
5418
|
if (path !== null && path.length >= 2) {
|
|
@@ -4893,7 +5655,7 @@ function routeEdge(input) {
|
|
|
4893
5655
|
};
|
|
4894
5656
|
}
|
|
4895
5657
|
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4896
|
-
const simplified =
|
|
5658
|
+
const simplified = simplifyRoute3(points);
|
|
4897
5659
|
if (simplified.length >= 3) {
|
|
4898
5660
|
return simplified;
|
|
4899
5661
|
}
|
|
@@ -5197,7 +5959,7 @@ function squaredDistance2(a, b) {
|
|
|
5197
5959
|
const dy = a.y - b.y;
|
|
5198
5960
|
return dx * dx + dy * dy;
|
|
5199
5961
|
}
|
|
5200
|
-
function
|
|
5962
|
+
function simplifyRoute3(points) {
|
|
5201
5963
|
const withoutDuplicates = [];
|
|
5202
5964
|
for (const point2 of points) {
|
|
5203
5965
|
const previous = withoutDuplicates.at(-1);
|
|
@@ -5748,7 +6510,21 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5748
6510
|
);
|
|
5749
6511
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
5750
6512
|
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
5751
|
-
const
|
|
6513
|
+
const useRecursive = options.recursiveLayout === true;
|
|
6514
|
+
if (useRecursive && initialLayoutMode === "positions") {
|
|
6515
|
+
diagnostics.push({
|
|
6516
|
+
severity: "warning",
|
|
6517
|
+
code: "layout.recursive-ignores-positions",
|
|
6518
|
+
message: 'recursiveLayout overrides initialLayout "positions" \u2014 seed positions are ignored for bottom-up container layout.'
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
6521
|
+
const layout2 = useRecursive ? runRecursiveContainerLayout({
|
|
6522
|
+
direction: diagram.direction,
|
|
6523
|
+
nodes: styledNodes,
|
|
6524
|
+
groups: styledGroups,
|
|
6525
|
+
edges: styledEdges,
|
|
6526
|
+
constraints
|
|
6527
|
+
}) : runInitialLayout({
|
|
5752
6528
|
mode: initialLayoutMode,
|
|
5753
6529
|
componentAware: options.maxStackDepth === void 0,
|
|
5754
6530
|
direction: diagram.direction,
|
|
@@ -5764,12 +6540,32 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5764
6540
|
options,
|
|
5765
6541
|
diagnostics
|
|
5766
6542
|
);
|
|
6543
|
+
if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
|
|
6544
|
+
const rewrapped = wrapHorizontalStackIfNeeded(
|
|
6545
|
+
initialNodeBoxes,
|
|
6546
|
+
styledNodes,
|
|
6547
|
+
diagram.direction,
|
|
6548
|
+
options,
|
|
6549
|
+
diagnostics
|
|
6550
|
+
);
|
|
6551
|
+
for (const [id, box] of rewrapped) {
|
|
6552
|
+
initialNodeBoxes.set(id, box);
|
|
6553
|
+
}
|
|
6554
|
+
}
|
|
6555
|
+
if (useRecursive && "groupBoxes" in layout2) {
|
|
6556
|
+
const recursiveLayout = layout2;
|
|
6557
|
+
for (const [groupId, groupBox] of recursiveLayout.groupBoxes) {
|
|
6558
|
+
initialNodeBoxes.set(groupId, groupBox);
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
5767
6561
|
expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
|
|
5768
6562
|
const constrained = applyLayoutConstraints({
|
|
5769
6563
|
direction: diagram.direction,
|
|
5770
6564
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
5771
6565
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
5772
6566
|
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
6567
|
+
distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
|
|
6568
|
+
swimlanes: styledSwimlanes,
|
|
5773
6569
|
boxes: initialNodeBoxes,
|
|
5774
6570
|
nodes: styledNodes,
|
|
5775
6571
|
constraints
|
|
@@ -6432,7 +7228,7 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
|
|
|
6432
7228
|
);
|
|
6433
7229
|
return wrapped;
|
|
6434
7230
|
}
|
|
6435
|
-
if (edges.length > 0 || !
|
|
7231
|
+
if (edges.length > 0 || !isStackRunaway(wrapped, nodes, direction, options)) {
|
|
6436
7232
|
reportVerticalRunaway(
|
|
6437
7233
|
wrapped,
|
|
6438
7234
|
nodes,
|
|
@@ -6483,8 +7279,57 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
|
|
|
6483
7279
|
});
|
|
6484
7280
|
return wrapped;
|
|
6485
7281
|
}
|
|
7282
|
+
function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnostics) {
|
|
7283
|
+
if (!isStackRunaway(boxes, nodes, direction, options)) {
|
|
7284
|
+
return new Map(boxes);
|
|
7285
|
+
}
|
|
7286
|
+
const maxRowDepth = options.maxRowDepth;
|
|
7287
|
+
if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
|
|
7288
|
+
return new Map(boxes);
|
|
7289
|
+
}
|
|
7290
|
+
const ordered = [...nodes].sort((a, b) => {
|
|
7291
|
+
const ba = boxes.get(a.id);
|
|
7292
|
+
const bb = boxes.get(b.id);
|
|
7293
|
+
if (ba === void 0 || bb === void 0) return 0;
|
|
7294
|
+
const dx = ba.x - bb.x;
|
|
7295
|
+
return dx !== 0 ? dx : ba.y - bb.y;
|
|
7296
|
+
});
|
|
7297
|
+
const rows = Math.ceil(ordered.length / maxRowDepth);
|
|
7298
|
+
const wrapped = new Map(boxes);
|
|
7299
|
+
const rowSpacing = options.overlapSpacing ?? 40;
|
|
7300
|
+
let minX = Infinity;
|
|
7301
|
+
let minY = Infinity;
|
|
7302
|
+
let maxH = 0;
|
|
7303
|
+
for (const n of ordered) {
|
|
7304
|
+
const b = boxes.get(n.id);
|
|
7305
|
+
if (b !== void 0) {
|
|
7306
|
+
minX = Math.min(minX, b.x);
|
|
7307
|
+
minY = Math.min(minY, b.y);
|
|
7308
|
+
maxH = Math.max(maxH, b.height);
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
for (let ri = 0; ri < rows; ri++) {
|
|
7312
|
+
const rowNodes = ordered.slice(ri * maxRowDepth, (ri + 1) * maxRowDepth);
|
|
7313
|
+
let x = minX;
|
|
7314
|
+
const y = minY + ri * (maxH + rowSpacing);
|
|
7315
|
+
for (const node of rowNodes) {
|
|
7316
|
+
const box = boxes.get(node.id);
|
|
7317
|
+
if (box === void 0) continue;
|
|
7318
|
+
wrapped.set(node.id, { ...box, x, y });
|
|
7319
|
+
x += box.width + rowSpacing;
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
diagnostics.push({
|
|
7323
|
+
severity: "warning",
|
|
7324
|
+
code: "horizontal_runaway",
|
|
7325
|
+
message: `Single-row layout exceeded maxRowDepth ${maxRowDepth}; wrapped into ${rows} rows.`,
|
|
7326
|
+
path: ["nodes"],
|
|
7327
|
+
detail: { nodeCount: ordered.length, maxRowDepth, rows }
|
|
7328
|
+
});
|
|
7329
|
+
return wrapped;
|
|
7330
|
+
}
|
|
6486
7331
|
function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
|
|
6487
|
-
if (!
|
|
7332
|
+
if (!isStackRunaway(boxes, nodes, direction, options)) {
|
|
6488
7333
|
return;
|
|
6489
7334
|
}
|
|
6490
7335
|
diagnostics.push({
|
|
@@ -6500,7 +7345,7 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
|
|
|
6500
7345
|
}
|
|
6501
7346
|
});
|
|
6502
7347
|
}
|
|
6503
|
-
function
|
|
7348
|
+
function isStackRunaway(boxes, nodes, direction, options) {
|
|
6504
7349
|
if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
|
|
6505
7350
|
return false;
|
|
6506
7351
|
}
|
|
@@ -7091,7 +7936,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
7091
7936
|
});
|
|
7092
7937
|
continue;
|
|
7093
7938
|
}
|
|
7094
|
-
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
|
|
7939
|
+
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
|
|
7095
7940
|
const geometry = computeShapeGeometry({
|
|
7096
7941
|
shape: node.shape,
|
|
7097
7942
|
box,
|
|
@@ -7185,7 +8030,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
|
|
|
7185
8030
|
}
|
|
7186
8031
|
}
|
|
7187
8032
|
}
|
|
7188
|
-
function coordinatePorts(node, nodeBox, portShifting) {
|
|
8033
|
+
function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
|
|
7189
8034
|
const portsBySide = /* @__PURE__ */ new Map();
|
|
7190
8035
|
for (const port of node.ports ?? []) {
|
|
7191
8036
|
const ports = portsBySide.get(port.side) ?? [];
|
|
@@ -7208,7 +8053,9 @@ function coordinatePorts(node, nodeBox, portShifting) {
|
|
|
7208
8053
|
side,
|
|
7209
8054
|
index,
|
|
7210
8055
|
sorted.length,
|
|
7211
|
-
portShifting
|
|
8056
|
+
portShifting,
|
|
8057
|
+
diagnostics,
|
|
8058
|
+
node.id
|
|
7212
8059
|
);
|
|
7213
8060
|
const box = portBox(anchor);
|
|
7214
8061
|
coordinated.push({ ...port, box, anchor });
|
|
@@ -7216,16 +8063,32 @@ function coordinatePorts(node, nodeBox, portShifting) {
|
|
|
7216
8063
|
}
|
|
7217
8064
|
return coordinated.sort((a, b) => a.id.localeCompare(b.id));
|
|
7218
8065
|
}
|
|
7219
|
-
function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
8066
|
+
function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
|
|
7220
8067
|
const shiftingEnabled = portShifting?.enabled ?? true;
|
|
7221
8068
|
const requestedSpacing = portShifting?.spacing ?? 24;
|
|
7222
8069
|
const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
|
|
7223
8070
|
const availableSpan = 2 * maxOffset;
|
|
7224
8071
|
const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
|
|
7225
|
-
const
|
|
8072
|
+
const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
|
|
7226
8073
|
Math.min(requestedSpacing, availableSpan / (count - 1)),
|
|
7227
8074
|
minSpacing
|
|
7228
8075
|
) : requestedSpacing;
|
|
8076
|
+
if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
|
|
8077
|
+
diagnostics.push({
|
|
8078
|
+
severity: "warning",
|
|
8079
|
+
code: "port_constraint_overlap",
|
|
8080
|
+
message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
|
|
8081
|
+
path: ["nodes", nodeId, "ports"],
|
|
8082
|
+
detail: {
|
|
8083
|
+
nodeId,
|
|
8084
|
+
side,
|
|
8085
|
+
requestedSpacing,
|
|
8086
|
+
effectiveSpacing: Math.round(effectiveSpacing),
|
|
8087
|
+
portCount: count
|
|
8088
|
+
}
|
|
8089
|
+
});
|
|
8090
|
+
}
|
|
8091
|
+
const spacing = effectiveSpacing;
|
|
7229
8092
|
const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
|
|
7230
8093
|
switch (side) {
|
|
7231
8094
|
case "left":
|
|
@@ -9036,6 +9899,7 @@ exports.applyLayoutConstraints = applyLayoutConstraints;
|
|
|
9036
9899
|
exports.assertFiniteNonNegative = assertFiniteNonNegative;
|
|
9037
9900
|
exports.assertFinitePositive = assertFinitePositive;
|
|
9038
9901
|
exports.boxCenter = boxCenter;
|
|
9902
|
+
exports.buildContainerTree = buildContainerTree;
|
|
9039
9903
|
exports.canonicalize = canonicalize;
|
|
9040
9904
|
exports.computeArrowhead = computeArrowhead;
|
|
9041
9905
|
exports.computeContainerGeometry = computeContainerGeometry;
|
|
@@ -9065,7 +9929,8 @@ exports.resolveOutputFormat = resolveOutputFormat;
|
|
|
9065
9929
|
exports.routeEdge = routeEdge;
|
|
9066
9930
|
exports.runComponentAwareDagreInitialLayout = runComponentAwareDagreInitialLayout;
|
|
9067
9931
|
exports.runDagreInitialLayout = runDagreInitialLayout;
|
|
9068
|
-
exports.
|
|
9932
|
+
exports.runRecursiveContainerLayout = runRecursiveContainerLayout;
|
|
9933
|
+
exports.simplifyRoute = simplifyRoute3;
|
|
9069
9934
|
exports.solveDiagram = solveDiagram;
|
|
9070
9935
|
exports.solveDiagramSafe = solveDiagramSafe;
|
|
9071
9936
|
exports.sortDslDiagnostics = sortDslDiagnostics;
|