@crazyhappyone/auto-graph 0.1.4 → 0.2.1
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 +475 -49
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +475 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +476 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +476 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -811,7 +811,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
811
811
|
const lock = locks.get(childId);
|
|
812
812
|
if (lock?.source === "fixed-position") {
|
|
813
813
|
unlocked.push({ id: childId, box });
|
|
814
|
-
locks.delete(childId);
|
|
815
814
|
continue;
|
|
816
815
|
}
|
|
817
816
|
diagnostics.push({
|
|
@@ -3984,6 +3983,240 @@ function isValidDimension(value) {
|
|
|
3984
3983
|
return Number.isFinite(value) && value >= 0;
|
|
3985
3984
|
}
|
|
3986
3985
|
|
|
3986
|
+
// src/routing/astar.ts
|
|
3987
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3988
|
+
const margin = options.margin ?? 0;
|
|
3989
|
+
const turnPenalty = options.turnPenalty ?? 50;
|
|
3990
|
+
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3991
|
+
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3992
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3993
|
+
const xs = collectXs(source, target, obstacles, margin);
|
|
3994
|
+
const ys = collectYs(source, target, obstacles, margin);
|
|
3995
|
+
if (xs.length * ys.length > maxNodes) {
|
|
3996
|
+
diagnostics?.push({
|
|
3997
|
+
severity: "warning",
|
|
3998
|
+
code: "routing.astar.grid_overflow",
|
|
3999
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
4000
|
+
detail: {
|
|
4001
|
+
xsCount: xs.length,
|
|
4002
|
+
ysCount: ys.length,
|
|
4003
|
+
maxNodes
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
4006
|
+
return null;
|
|
4007
|
+
}
|
|
4008
|
+
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
4009
|
+
connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
|
|
4010
|
+
connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
|
|
4011
|
+
const path = aStarSearch(
|
|
4012
|
+
nodes,
|
|
4013
|
+
nodeIndex,
|
|
4014
|
+
source,
|
|
4015
|
+
target,
|
|
4016
|
+
turnPenalty,
|
|
4017
|
+
segmentPenalty
|
|
4018
|
+
);
|
|
4019
|
+
if (path === null) return null;
|
|
4020
|
+
return simplifyRoute(path);
|
|
4021
|
+
}
|
|
4022
|
+
function collectXs(source, target, obstacles, margin) {
|
|
4023
|
+
const raw = [];
|
|
4024
|
+
for (const obs of obstacles) {
|
|
4025
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
4026
|
+
}
|
|
4027
|
+
const deduped = dedupSorted(raw);
|
|
4028
|
+
for (const v of [source.x, target.x]) {
|
|
4029
|
+
if (!deduped.includes(v)) {
|
|
4030
|
+
deduped.push(v);
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
return deduped.sort((a, b) => a - b);
|
|
4034
|
+
}
|
|
4035
|
+
function collectYs(source, target, obstacles, margin) {
|
|
4036
|
+
const raw = [];
|
|
4037
|
+
for (const obs of obstacles) {
|
|
4038
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
4039
|
+
}
|
|
4040
|
+
const deduped = dedupSorted(raw);
|
|
4041
|
+
for (const v of [source.y, target.y]) {
|
|
4042
|
+
if (!deduped.includes(v)) {
|
|
4043
|
+
deduped.push(v);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return deduped.sort((a, b) => a - b);
|
|
4047
|
+
}
|
|
4048
|
+
function dedupSorted(values) {
|
|
4049
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4050
|
+
const result = [];
|
|
4051
|
+
for (const v of sorted) {
|
|
4052
|
+
const last = result[result.length - 1];
|
|
4053
|
+
if (last === void 0 || v - last > 2) {
|
|
4054
|
+
result.push(v);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
return result;
|
|
4058
|
+
}
|
|
4059
|
+
function buildGraph(xs, ys) {
|
|
4060
|
+
const nodes = [];
|
|
4061
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
4062
|
+
for (let xi = 0; xi < xs.length; xi++) {
|
|
4063
|
+
for (let yi = 0; yi < ys.length; yi++) {
|
|
4064
|
+
const x = xs[xi];
|
|
4065
|
+
const y = ys[yi];
|
|
4066
|
+
const id = nodes.length;
|
|
4067
|
+
nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
|
|
4068
|
+
nodeIndex.set(`${x},${y}`, id);
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
return { nodes, nodeIndex };
|
|
4072
|
+
}
|
|
4073
|
+
function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
|
|
4074
|
+
for (const y of ys) {
|
|
4075
|
+
const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
|
|
4076
|
+
for (let i = 0; i < row.length - 1; i++) {
|
|
4077
|
+
const a = row[i];
|
|
4078
|
+
const b = row[i + 1];
|
|
4079
|
+
const dx = b.x - a.x;
|
|
4080
|
+
if (dx <= 0) continue;
|
|
4081
|
+
if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
|
|
4082
|
+
continue;
|
|
4083
|
+
}
|
|
4084
|
+
a.neighbors.set(b.id, dx);
|
|
4085
|
+
b.neighbors.set(a.id, dx);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
|
|
4090
|
+
for (const x of xs) {
|
|
4091
|
+
const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
|
|
4092
|
+
for (let i = 0; i < col.length - 1; i++) {
|
|
4093
|
+
const a = col[i];
|
|
4094
|
+
const b = col[i + 1];
|
|
4095
|
+
const dy = b.y - a.y;
|
|
4096
|
+
if (dy <= 0) continue;
|
|
4097
|
+
if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
|
|
4098
|
+
continue;
|
|
4099
|
+
}
|
|
4100
|
+
a.neighbors.set(b.id, dy);
|
|
4101
|
+
b.neighbors.set(a.id, dy);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
|
|
4106
|
+
for (const obs of obstacles) {
|
|
4107
|
+
if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
|
|
4108
|
+
}
|
|
4109
|
+
for (const ep of endpointObstacles) {
|
|
4110
|
+
if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
|
|
4111
|
+
}
|
|
4112
|
+
return false;
|
|
4113
|
+
}
|
|
4114
|
+
function segmentCrossesBoxStrict(start, end, box, margin) {
|
|
4115
|
+
const left = box.x - margin;
|
|
4116
|
+
const right = box.x + box.width + margin;
|
|
4117
|
+
const top = box.y - margin;
|
|
4118
|
+
const bottom = box.y + box.height + margin;
|
|
4119
|
+
if (pointInsideStrict(start, left, right, top, bottom)) return true;
|
|
4120
|
+
if (pointInsideStrict(end, left, right, top, bottom)) return true;
|
|
4121
|
+
if (start.x === end.x) {
|
|
4122
|
+
return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
|
|
4123
|
+
}
|
|
4124
|
+
if (start.y === end.y) {
|
|
4125
|
+
return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
|
|
4126
|
+
}
|
|
4127
|
+
return segmentEdgeIntersect(start, end, left, top, right, top) || segmentEdgeIntersect(start, end, right, top, right, bottom) || segmentEdgeIntersect(start, end, right, bottom, left, bottom) || segmentEdgeIntersect(start, end, left, bottom, left, top);
|
|
4128
|
+
}
|
|
4129
|
+
function pointInsideStrict(p, left, right, top, bottom) {
|
|
4130
|
+
return p.x > left && p.x < right && p.y > top && p.y < bottom;
|
|
4131
|
+
}
|
|
4132
|
+
function rangesOverlap(a, b, min, max) {
|
|
4133
|
+
const low = Math.min(a, b);
|
|
4134
|
+
const high = Math.max(a, b);
|
|
4135
|
+
return high > min && low < max;
|
|
4136
|
+
}
|
|
4137
|
+
function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
|
|
4138
|
+
const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
|
|
4139
|
+
if (denominator === 0) return false;
|
|
4140
|
+
const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
|
|
4141
|
+
const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
|
|
4142
|
+
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
4143
|
+
}
|
|
4144
|
+
function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
|
|
4145
|
+
const startId = nodeIndex.get(`${source.x},${source.y}`);
|
|
4146
|
+
const goalId = nodeIndex.get(`${target.x},${target.y}`);
|
|
4147
|
+
if (startId === void 0 || goalId === void 0) return null;
|
|
4148
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
4149
|
+
gScore.set(startId, 0);
|
|
4150
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
4151
|
+
const cameFromDir = /* @__PURE__ */ new Map();
|
|
4152
|
+
const openSet = [];
|
|
4153
|
+
openSet.push({
|
|
4154
|
+
id: startId,
|
|
4155
|
+
f: manhattan(source, target)
|
|
4156
|
+
});
|
|
4157
|
+
while (openSet.length > 0) {
|
|
4158
|
+
let bestIdx = 0;
|
|
4159
|
+
for (let i = 1; i < openSet.length; i++) {
|
|
4160
|
+
if (openSet[i].f < openSet[bestIdx].f) {
|
|
4161
|
+
bestIdx = i;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
const current = openSet.splice(bestIdx, 1)[0];
|
|
4165
|
+
if (current.id === goalId) {
|
|
4166
|
+
return reconstructPath(nodes, cameFrom, goalId);
|
|
4167
|
+
}
|
|
4168
|
+
const node = nodes[current.id];
|
|
4169
|
+
const currentG = gScore.get(current.id);
|
|
4170
|
+
const prevDir = cameFromDir.get(current.id);
|
|
4171
|
+
for (const [neighborId, edgeCost] of node.neighbors) {
|
|
4172
|
+
const neighbor = nodes[neighborId];
|
|
4173
|
+
const tentativeG = currentG + edgeCost * segmentPenalty;
|
|
4174
|
+
const newDir = neighbor.y === node.y ? "h" : "v";
|
|
4175
|
+
const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
|
|
4176
|
+
const totalG = tentativeG + turnCost;
|
|
4177
|
+
const existingG = gScore.get(neighborId);
|
|
4178
|
+
if (existingG === void 0 || totalG < existingG) {
|
|
4179
|
+
gScore.set(neighborId, totalG);
|
|
4180
|
+
cameFrom.set(neighborId, current.id);
|
|
4181
|
+
cameFromDir.set(neighborId, newDir);
|
|
4182
|
+
const f = totalG + manhattan(neighbor, target);
|
|
4183
|
+
openSet.push({ id: neighborId, f });
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
return null;
|
|
4188
|
+
}
|
|
4189
|
+
function manhattan(a, b) {
|
|
4190
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
4191
|
+
}
|
|
4192
|
+
function reconstructPath(nodes, cameFrom, goalId) {
|
|
4193
|
+
const path = [];
|
|
4194
|
+
let current = goalId;
|
|
4195
|
+
while (current !== void 0) {
|
|
4196
|
+
const node = nodes[current];
|
|
4197
|
+
path.unshift({ x: node.x, y: node.y });
|
|
4198
|
+
current = cameFrom.get(current);
|
|
4199
|
+
}
|
|
4200
|
+
return path;
|
|
4201
|
+
}
|
|
4202
|
+
function simplifyRoute(points) {
|
|
4203
|
+
if (points.length <= 2) return [...points];
|
|
4204
|
+
const result = [points[0]];
|
|
4205
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
4206
|
+
const prev = result[result.length - 1];
|
|
4207
|
+
const curr = points[i];
|
|
4208
|
+
const next = points[i + 1];
|
|
4209
|
+
if (!areCollinear(prev, curr, next)) {
|
|
4210
|
+
result.push(curr);
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
result.push(points[points.length - 1]);
|
|
4214
|
+
return result;
|
|
4215
|
+
}
|
|
4216
|
+
function areCollinear(a, b, c) {
|
|
4217
|
+
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
3987
4220
|
// src/routing/routes.ts
|
|
3988
4221
|
function routeEdge(input) {
|
|
3989
4222
|
const diagnostics = [];
|
|
@@ -4029,6 +4262,44 @@ function routeEdge(input) {
|
|
|
4029
4262
|
}
|
|
4030
4263
|
return { points, diagnostics };
|
|
4031
4264
|
}
|
|
4265
|
+
if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
|
|
4266
|
+
const endpointObstacles = endpointObstaclesForAutoAnchors(input);
|
|
4267
|
+
for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
|
|
4268
|
+
input,
|
|
4269
|
+
defaultAnchors
|
|
4270
|
+
)) {
|
|
4271
|
+
const source = getEdgePort(
|
|
4272
|
+
input.source,
|
|
4273
|
+
input.target.center,
|
|
4274
|
+
sourceAnchor
|
|
4275
|
+
);
|
|
4276
|
+
const target = getEdgePort(
|
|
4277
|
+
input.target,
|
|
4278
|
+
input.source.center,
|
|
4279
|
+
targetAnchor
|
|
4280
|
+
);
|
|
4281
|
+
const path = findObstacleFreePath(
|
|
4282
|
+
source,
|
|
4283
|
+
target,
|
|
4284
|
+
[...softObstacles, ...hardObstacles],
|
|
4285
|
+
{
|
|
4286
|
+
endpointObstacles
|
|
4287
|
+
},
|
|
4288
|
+
diagnostics
|
|
4289
|
+
);
|
|
4290
|
+
if (path !== null && path.length >= 2) {
|
|
4291
|
+
const finalized = finalizeRoute(
|
|
4292
|
+
path,
|
|
4293
|
+
softObstacles,
|
|
4294
|
+
hardObstacles,
|
|
4295
|
+
diagnostics
|
|
4296
|
+
);
|
|
4297
|
+
if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
|
|
4298
|
+
return { points: finalized, diagnostics };
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4032
4303
|
const routeLaneObstacles = [...softObstacles, ...hardObstacles];
|
|
4033
4304
|
const anchorPairs = routeAnchorPairs(input, defaultAnchors);
|
|
4034
4305
|
const candidateRoutes = anchorPairs.flatMap(
|
|
@@ -4231,7 +4502,7 @@ function routeEdge(input) {
|
|
|
4231
4502
|
};
|
|
4232
4503
|
}
|
|
4233
4504
|
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4234
|
-
const simplified =
|
|
4505
|
+
const simplified = simplifyRoute2(points);
|
|
4235
4506
|
if (simplified.length >= 3) {
|
|
4236
4507
|
return simplified;
|
|
4237
4508
|
}
|
|
@@ -4519,7 +4790,7 @@ function squaredDistance2(a, b) {
|
|
|
4519
4790
|
const dy = a.y - b.y;
|
|
4520
4791
|
return dx * dx + dy * dy;
|
|
4521
4792
|
}
|
|
4522
|
-
function
|
|
4793
|
+
function simplifyRoute2(points) {
|
|
4523
4794
|
const withoutDuplicates = [];
|
|
4524
4795
|
for (const point2 of points) {
|
|
4525
4796
|
const previous = withoutDuplicates.at(-1);
|
|
@@ -4531,7 +4802,7 @@ function simplifyRoute(points) {
|
|
|
4531
4802
|
for (const point2 of withoutDuplicates) {
|
|
4532
4803
|
const previous = simplified.at(-1);
|
|
4533
4804
|
const beforePrevious = simplified.at(-2);
|
|
4534
|
-
if (previous !== void 0 && beforePrevious !== void 0 &&
|
|
4805
|
+
if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
|
|
4535
4806
|
simplified[simplified.length - 1] = { ...point2 };
|
|
4536
4807
|
} else {
|
|
4537
4808
|
simplified.push({ ...point2 });
|
|
@@ -4746,17 +5017,17 @@ function segmentIntersectsBox(start, end, box) {
|
|
|
4746
5017
|
return true;
|
|
4747
5018
|
}
|
|
4748
5019
|
if (start.x === end.x) {
|
|
4749
|
-
return start.x > left && start.x < right &&
|
|
5020
|
+
return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
|
|
4750
5021
|
}
|
|
4751
5022
|
if (start.y === end.y) {
|
|
4752
|
-
return start.y > top && start.y < bottom &&
|
|
5023
|
+
return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
|
|
4753
5024
|
}
|
|
4754
5025
|
return segmentIntersectsBoxEdge(start, end, left, top, right, top) || segmentIntersectsBoxEdge(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge(start, end, left, bottom, left, top);
|
|
4755
5026
|
}
|
|
4756
5027
|
function pointInsideBox(point2, box) {
|
|
4757
5028
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
4758
5029
|
}
|
|
4759
|
-
function
|
|
5030
|
+
function rangesOverlap2(a, b, min, max) {
|
|
4760
5031
|
const low = Math.min(a, b);
|
|
4761
5032
|
const high = Math.max(a, b);
|
|
4762
5033
|
return high > min && low < max;
|
|
@@ -4780,7 +5051,7 @@ function segmentBox(a, b) {
|
|
|
4780
5051
|
height: Math.max(1, Math.abs(a.y - b.y))
|
|
4781
5052
|
};
|
|
4782
5053
|
}
|
|
4783
|
-
function
|
|
5054
|
+
function areCollinear2(a, b, c) {
|
|
4784
5055
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
4785
5056
|
}
|
|
4786
5057
|
|
|
@@ -4860,11 +5131,12 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4860
5131
|
options,
|
|
4861
5132
|
diagnostics
|
|
4862
5133
|
);
|
|
5134
|
+
expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
|
|
4863
5135
|
const constrained = applyLayoutConstraints({
|
|
4864
5136
|
direction: diagram.direction,
|
|
4865
5137
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
4866
5138
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
4867
|
-
|
|
5139
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
4868
5140
|
boxes: initialNodeBoxes,
|
|
4869
5141
|
nodes: styledNodes,
|
|
4870
5142
|
constraints
|
|
@@ -4923,6 +5195,11 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4923
5195
|
swimlanes: coordinatedSwimlanes,
|
|
4924
5196
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
4925
5197
|
});
|
|
5198
|
+
const edgeLabelEstimates = estimateEdgeLabelAnnotations(
|
|
5199
|
+
styledEdges,
|
|
5200
|
+
nodeGeometryById,
|
|
5201
|
+
options.textMeasurer
|
|
5202
|
+
);
|
|
4926
5203
|
const layoutBoxes = [
|
|
4927
5204
|
...coordinatedNodes.map((node) => node.box),
|
|
4928
5205
|
...coordinatedNodes.flatMap(
|
|
@@ -5003,7 +5280,10 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5003
5280
|
const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
|
|
5004
5281
|
const routingTextObstacles = [
|
|
5005
5282
|
...baseTextAnnotations.filter(isPreRouteTextObstacle),
|
|
5006
|
-
...frameTextAnnotation.filter(isPreRouteTextObstacle)
|
|
5283
|
+
...frameTextAnnotation.filter(isPreRouteTextObstacle),
|
|
5284
|
+
// Dry-run edge-label estimates so edges route around
|
|
5285
|
+
// each other's label areas (Issue #41).
|
|
5286
|
+
...edgeLabelEstimates
|
|
5007
5287
|
];
|
|
5008
5288
|
const margin = options.obstacleMargin ?? 0;
|
|
5009
5289
|
const softObstacles = [
|
|
@@ -5036,7 +5316,8 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5036
5316
|
hardObstacles,
|
|
5037
5317
|
diagram.direction,
|
|
5038
5318
|
options,
|
|
5039
|
-
diagnostics
|
|
5319
|
+
diagnostics,
|
|
5320
|
+
coordinatedGroups
|
|
5040
5321
|
);
|
|
5041
5322
|
const edgeTextAnnotations = coordinateEdgeTextAnnotations(
|
|
5042
5323
|
coordinatedEdges,
|
|
@@ -5152,22 +5433,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
|
|
|
5152
5433
|
y: layout2.box.y + offsetY,
|
|
5153
5434
|
width: layout2.box.width,
|
|
5154
5435
|
height: layout2.box.height
|
|
5155
|
-
}
|
|
5156
|
-
contentBox: {
|
|
5157
|
-
x: layout2.contentBox.x + offsetX,
|
|
5158
|
-
y: layout2.contentBox.y + offsetY,
|
|
5159
|
-
width: layout2.contentBox.width,
|
|
5160
|
-
height: layout2.contentBox.height
|
|
5161
|
-
},
|
|
5162
|
-
lines: layout2.lines.map((line) => ({
|
|
5163
|
-
...line,
|
|
5164
|
-
box: {
|
|
5165
|
-
x: line.box.x + offsetX,
|
|
5166
|
-
y: line.box.y + offsetY,
|
|
5167
|
-
width: line.box.width,
|
|
5168
|
-
height: line.box.height
|
|
5169
|
-
}
|
|
5170
|
-
}))
|
|
5436
|
+
}
|
|
5171
5437
|
};
|
|
5172
5438
|
}
|
|
5173
5439
|
function reportPageOverflow(contentBounds, pageBounds) {
|
|
@@ -6114,6 +6380,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
6114
6380
|
});
|
|
6115
6381
|
continue;
|
|
6116
6382
|
}
|
|
6383
|
+
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
|
|
6117
6384
|
const geometry = computeShapeGeometry({
|
|
6118
6385
|
shape: node.shape,
|
|
6119
6386
|
box,
|
|
@@ -6123,7 +6390,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
6123
6390
|
id: node.id,
|
|
6124
6391
|
...node.label === void 0 ? {} : { label: node.label },
|
|
6125
6392
|
...node.style === void 0 ? {} : { style: node.style },
|
|
6126
|
-
...
|
|
6393
|
+
...ports === void 0 ? {} : { ports },
|
|
6127
6394
|
...node.compartments === void 0 ? {} : { compartments: node.compartments },
|
|
6128
6395
|
...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
|
|
6129
6396
|
shape: node.shape,
|
|
@@ -6135,6 +6402,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
6135
6402
|
}
|
|
6136
6403
|
return coordinated;
|
|
6137
6404
|
}
|
|
6405
|
+
var PORT_BOX_SIZE = 10;
|
|
6406
|
+
var MIN_PORT_EDGE_GAP = 12;
|
|
6407
|
+
function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
|
|
6408
|
+
const shiftingEnabled = options.portShifting?.enabled ?? true;
|
|
6409
|
+
if (!shiftingEnabled) return;
|
|
6410
|
+
const requestedSpacing = options.portShifting?.spacing ?? 24;
|
|
6411
|
+
const minSpacing = Math.max(
|
|
6412
|
+
requestedSpacing,
|
|
6413
|
+
PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
|
|
6414
|
+
);
|
|
6415
|
+
for (const node of nodes) {
|
|
6416
|
+
if (node.ports === void 0 || node.ports.length === 0) continue;
|
|
6417
|
+
const box = boxes.get(node.id);
|
|
6418
|
+
if (box === void 0) continue;
|
|
6419
|
+
let heightExpansion = 0;
|
|
6420
|
+
let widthExpansion = 0;
|
|
6421
|
+
const portsBySide = /* @__PURE__ */ new Map();
|
|
6422
|
+
for (const port of node.ports) {
|
|
6423
|
+
const list = portsBySide.get(port.side) ?? [];
|
|
6424
|
+
list.push(port);
|
|
6425
|
+
portsBySide.set(port.side, list);
|
|
6426
|
+
}
|
|
6427
|
+
for (const [side, ports] of portsBySide) {
|
|
6428
|
+
const count = (ports ?? []).length;
|
|
6429
|
+
if (count <= 1) continue;
|
|
6430
|
+
const isVertical = side === "left" || side === "right";
|
|
6431
|
+
const availableSpan = isVertical ? box.height : box.width;
|
|
6432
|
+
const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
|
|
6433
|
+
if (requiredSpan > availableSpan) {
|
|
6434
|
+
const expansion = requiredSpan - availableSpan;
|
|
6435
|
+
if (isVertical) {
|
|
6436
|
+
heightExpansion = Math.max(heightExpansion, expansion);
|
|
6437
|
+
} else {
|
|
6438
|
+
widthExpansion = Math.max(widthExpansion, expansion);
|
|
6439
|
+
}
|
|
6440
|
+
diagnostics.push({
|
|
6441
|
+
severity: "info",
|
|
6442
|
+
code: "port_capacity_overflow",
|
|
6443
|
+
message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
|
|
6444
|
+
path: ["nodes", node.id, "ports"],
|
|
6445
|
+
detail: {
|
|
6446
|
+
nodeId: node.id,
|
|
6447
|
+
side,
|
|
6448
|
+
portCount: count,
|
|
6449
|
+
expansion: Math.ceil(expansion)
|
|
6450
|
+
}
|
|
6451
|
+
});
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
if (heightExpansion > 0) {
|
|
6455
|
+
box.y -= heightExpansion / 2;
|
|
6456
|
+
box.height += heightExpansion;
|
|
6457
|
+
}
|
|
6458
|
+
if (widthExpansion > 0) {
|
|
6459
|
+
box.x -= widthExpansion / 2;
|
|
6460
|
+
box.width += widthExpansion;
|
|
6461
|
+
}
|
|
6462
|
+
if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
|
|
6463
|
+
const layout2 = node.labelLayout;
|
|
6464
|
+
const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
|
|
6465
|
+
const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
|
|
6466
|
+
node.labelLayout = {
|
|
6467
|
+
...layout2,
|
|
6468
|
+
box: {
|
|
6469
|
+
...layout2.box,
|
|
6470
|
+
x: newOffsetX,
|
|
6471
|
+
y: newOffsetY
|
|
6472
|
+
}
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6138
6477
|
function coordinatePorts(node, nodeBox, portShifting) {
|
|
6139
6478
|
const portsBySide = /* @__PURE__ */ new Map();
|
|
6140
6479
|
for (const port of node.ports ?? []) {
|
|
@@ -6171,7 +6510,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
|
6171
6510
|
const requestedSpacing = portShifting?.spacing ?? 24;
|
|
6172
6511
|
const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
|
|
6173
6512
|
const availableSpan = 2 * maxOffset;
|
|
6174
|
-
const
|
|
6513
|
+
const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
|
|
6514
|
+
const spacing = shiftingEnabled && count > 1 ? Math.max(
|
|
6515
|
+
Math.min(requestedSpacing, availableSpan / (count - 1)),
|
|
6516
|
+
minSpacing
|
|
6517
|
+
) : requestedSpacing;
|
|
6175
6518
|
const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
|
|
6176
6519
|
switch (side) {
|
|
6177
6520
|
case "left":
|
|
@@ -6197,7 +6540,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
|
6197
6540
|
}
|
|
6198
6541
|
}
|
|
6199
6542
|
function portBox(anchor) {
|
|
6200
|
-
const size =
|
|
6543
|
+
const size = PORT_BOX_SIZE;
|
|
6201
6544
|
return {
|
|
6202
6545
|
x: anchor.x - size / 2,
|
|
6203
6546
|
y: anchor.y - size / 2,
|
|
@@ -6786,7 +7129,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
|
|
|
6786
7129
|
}
|
|
6787
7130
|
};
|
|
6788
7131
|
}
|
|
6789
|
-
function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
|
|
7132
|
+
function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
|
|
6790
7133
|
const coordinated = [];
|
|
6791
7134
|
const coordinatedNodeById = new Map(
|
|
6792
7135
|
coordinatedNodes.map((node) => [node.id, node])
|
|
@@ -6810,8 +7153,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6810
7153
|
}
|
|
6811
7154
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
6812
7155
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
6813
|
-
const
|
|
6814
|
-
const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
|
|
7156
|
+
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
6815
7157
|
const route = routeEdge({
|
|
6816
7158
|
kind: options.routeKind ?? "orthogonal",
|
|
6817
7159
|
direction,
|
|
@@ -6824,6 +7166,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6824
7166
|
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
6825
7167
|
),
|
|
6826
7168
|
...softObstacles,
|
|
7169
|
+
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
6827
7170
|
...routeTextObstacles
|
|
6828
7171
|
],
|
|
6829
7172
|
hardObstacles,
|
|
@@ -6842,15 +7185,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6842
7185
|
}
|
|
6843
7186
|
return coordinated;
|
|
6844
7187
|
}
|
|
6845
|
-
function
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
7188
|
+
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
7189
|
+
switch (annotation.surfaceKind) {
|
|
7190
|
+
case "edge-label":
|
|
7191
|
+
return annotation.ownerId === edge.id;
|
|
7192
|
+
case "node-label":
|
|
7193
|
+
case "compartment-row":
|
|
7194
|
+
return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
|
|
7195
|
+
case "port-label":
|
|
7196
|
+
return edge.source.portId !== void 0 && annotation.ownerId === `${edge.source.nodeId}.${edge.source.portId}` || edge.target.portId !== void 0 && annotation.ownerId === `${edge.target.nodeId}.${edge.target.portId}`;
|
|
7197
|
+
case "group-label":
|
|
7198
|
+
case "swimlane-label":
|
|
7199
|
+
case "frame-title":
|
|
7200
|
+
return false;
|
|
6849
7201
|
}
|
|
6850
|
-
|
|
6851
|
-
|
|
7202
|
+
}
|
|
7203
|
+
function ancestorGroupIds(groups, nodeId) {
|
|
7204
|
+
const direct = /* @__PURE__ */ new Set();
|
|
7205
|
+
for (const group of groups) {
|
|
7206
|
+
if (group.nodeIds.includes(nodeId)) {
|
|
7207
|
+
direct.add(group.id);
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
let previousSize = -1;
|
|
7211
|
+
const ancestors = new Set(direct);
|
|
7212
|
+
while (ancestors.size !== previousSize) {
|
|
7213
|
+
previousSize = ancestors.size;
|
|
7214
|
+
for (const group of groups) {
|
|
7215
|
+
for (const candidate of ancestors) {
|
|
7216
|
+
if (group.groupIds.includes(candidate)) {
|
|
7217
|
+
ancestors.add(group.id);
|
|
7218
|
+
break;
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
6852
7222
|
}
|
|
6853
|
-
return
|
|
7223
|
+
return ancestors;
|
|
7224
|
+
}
|
|
7225
|
+
function groupObstaclesForEdge(edge, groups, margin) {
|
|
7226
|
+
const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
|
|
7227
|
+
const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
|
|
7228
|
+
return groups.filter((group) => {
|
|
7229
|
+
if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
|
|
7230
|
+
return false;
|
|
7231
|
+
}
|
|
7232
|
+
return true;
|
|
7233
|
+
}).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
|
|
6854
7234
|
}
|
|
6855
7235
|
function coordinateBaseTextAnnotations(input) {
|
|
6856
7236
|
const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
|
|
@@ -7038,6 +7418,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
|
|
|
7038
7418
|
}
|
|
7039
7419
|
return annotations;
|
|
7040
7420
|
}
|
|
7421
|
+
function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
|
|
7422
|
+
const measurer = textMeasurer ?? createDefaultTextMeasurer();
|
|
7423
|
+
const annotations = [];
|
|
7424
|
+
for (const edge of edges) {
|
|
7425
|
+
if (edge.label?.text === void 0) {
|
|
7426
|
+
continue;
|
|
7427
|
+
}
|
|
7428
|
+
const sourceGeom = nodes.get(edge.source.nodeId);
|
|
7429
|
+
const targetGeom = nodes.get(edge.target.nodeId);
|
|
7430
|
+
if (sourceGeom === void 0 || targetGeom === void 0) {
|
|
7431
|
+
continue;
|
|
7432
|
+
}
|
|
7433
|
+
const layout2 = fitLabel(
|
|
7434
|
+
edge.label.text,
|
|
7435
|
+
{
|
|
7436
|
+
font: typographyTextStyle(edge.label, {
|
|
7437
|
+
fontFamily: "Arial",
|
|
7438
|
+
fontSize: 12,
|
|
7439
|
+
lineHeight: 14
|
|
7440
|
+
}),
|
|
7441
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
7442
|
+
minSize: { width: 0, height: 0 },
|
|
7443
|
+
maxWidth: 200
|
|
7444
|
+
},
|
|
7445
|
+
measurer
|
|
7446
|
+
);
|
|
7447
|
+
const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
|
|
7448
|
+
const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
|
|
7449
|
+
const box = {
|
|
7450
|
+
x: cx - layout2.box.width / 2,
|
|
7451
|
+
y: cy - layout2.box.height / 2,
|
|
7452
|
+
width: layout2.box.width,
|
|
7453
|
+
height: layout2.box.height
|
|
7454
|
+
};
|
|
7455
|
+
annotations.push({
|
|
7456
|
+
text: layout2.text,
|
|
7457
|
+
ownerId: edge.id,
|
|
7458
|
+
surfaceKind: "edge-label",
|
|
7459
|
+
box,
|
|
7460
|
+
anchor: { x: cx, y: cy },
|
|
7461
|
+
paddings: layout2.padding,
|
|
7462
|
+
lines: layout2.lines,
|
|
7463
|
+
fontFamily: normalizeOutputFontFamily(layout2.font),
|
|
7464
|
+
fontSize: layout2.font.fontSize,
|
|
7465
|
+
textBackend: layout2.textBackend
|
|
7466
|
+
});
|
|
7467
|
+
}
|
|
7468
|
+
return annotations;
|
|
7469
|
+
}
|
|
7041
7470
|
function coordinateFrameTextAnnotation(frame, textMeasurer) {
|
|
7042
7471
|
const layout2 = fitLabel(
|
|
7043
7472
|
frame.titleTab,
|
|
@@ -7158,9 +7587,8 @@ function reportRouteTextClearance(edges, annotations) {
|
|
|
7158
7587
|
const diagnostics = [];
|
|
7159
7588
|
const relevantAnnotations = annotations.filter(isRouteClearanceText);
|
|
7160
7589
|
for (const edge of edges) {
|
|
7161
|
-
const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
|
|
7162
7590
|
for (const annotation of relevantAnnotations) {
|
|
7163
|
-
if (
|
|
7591
|
+
if (isEdgeConnectedTextAnnotation(edge, annotation)) {
|
|
7164
7592
|
continue;
|
|
7165
7593
|
}
|
|
7166
7594
|
if (!routeIntersectsTextBox(edge.points, annotation.box)) {
|
|
@@ -7184,9 +7612,6 @@ function reportRouteTextClearance(edges, annotations) {
|
|
|
7184
7612
|
return diagnostics;
|
|
7185
7613
|
}
|
|
7186
7614
|
function isPreRouteTextObstacle(annotation) {
|
|
7187
|
-
if (annotation.surfaceKind === "edge-label") {
|
|
7188
|
-
return false;
|
|
7189
|
-
}
|
|
7190
7615
|
return isRouteClearanceText(annotation);
|
|
7191
7616
|
}
|
|
7192
7617
|
function isRouteClearanceText(annotation) {
|
|
@@ -7197,8 +7622,9 @@ function isRouteClearanceText(annotation) {
|
|
|
7197
7622
|
case "frame-title":
|
|
7198
7623
|
return true;
|
|
7199
7624
|
case "node-label":
|
|
7200
|
-
case "group-label":
|
|
7201
7625
|
case "compartment-row":
|
|
7626
|
+
return true;
|
|
7627
|
+
case "group-label":
|
|
7202
7628
|
return textExtendsOutsideAnchor(annotation);
|
|
7203
7629
|
}
|
|
7204
7630
|
}
|
|
@@ -7231,17 +7657,17 @@ function segmentIntersectsBox2(start, end, box) {
|
|
|
7231
7657
|
return true;
|
|
7232
7658
|
}
|
|
7233
7659
|
if (start.x === end.x) {
|
|
7234
|
-
return start.x > left && start.x < right &&
|
|
7660
|
+
return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
|
|
7235
7661
|
}
|
|
7236
7662
|
if (start.y === end.y) {
|
|
7237
|
-
return start.y > top && start.y < bottom &&
|
|
7663
|
+
return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
|
|
7238
7664
|
}
|
|
7239
7665
|
return segmentIntersectsBoxEdge2(start, end, left, top, right, top) || segmentIntersectsBoxEdge2(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge2(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge2(start, end, left, bottom, left, top);
|
|
7240
7666
|
}
|
|
7241
7667
|
function pointInsideBox2(point2, box) {
|
|
7242
7668
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
7243
7669
|
}
|
|
7244
|
-
function
|
|
7670
|
+
function rangesOverlap3(a, b, min, max) {
|
|
7245
7671
|
const low = Math.min(a, b);
|
|
7246
7672
|
const high = Math.max(a, b);
|
|
7247
7673
|
return high > min && low < max;
|
|
@@ -7869,7 +8295,7 @@ exports.resolveLineHeight = resolveLineHeight;
|
|
|
7869
8295
|
exports.resolveOutputFormat = resolveOutputFormat;
|
|
7870
8296
|
exports.routeEdge = routeEdge;
|
|
7871
8297
|
exports.runDagreInitialLayout = runDagreInitialLayout;
|
|
7872
|
-
exports.simplifyRoute =
|
|
8298
|
+
exports.simplifyRoute = simplifyRoute2;
|
|
7873
8299
|
exports.solveDiagram = solveDiagram;
|
|
7874
8300
|
exports.solveDiagramSafe = solveDiagramSafe;
|
|
7875
8301
|
exports.sortDslDiagnostics = sortDslDiagnostics;
|