@statelyai/layout 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +43 -0
- package/README.md +82 -4
- package/dist/elkjs/index.cjs +8 -0
- package/dist/elkjs/index.d.cts +146 -0
- package/dist/elkjs/index.d.mts +42 -39
- package/dist/elkjs/index.mjs +2 -1483
- package/dist/elkjs/main.cjs +17 -0
- package/dist/elkjs/main.d.cts +10 -0
- package/dist/elkjs/main.d.mts +11 -0
- package/dist/elkjs/main.mjs +17 -0
- package/dist/elkjs/worker-api.cjs +3 -0
- package/dist/elkjs/worker-api.d.cts +3 -0
- package/dist/elkjs/worker-api.d.mts +3 -0
- package/dist/elkjs/worker-api.mjs +3 -0
- package/dist/elkjs/worker.cjs +86 -0
- package/dist/elkjs/worker.d.cts +17 -0
- package/dist/elkjs/worker.d.mts +17 -0
- package/dist/elkjs/worker.mjs +86 -0
- package/dist/elkjs-BVnDwnBQ.cjs +1551 -0
- package/dist/elkjs-CFlfkEBz.mjs +1528 -0
- package/dist/{index-8xYkohbz.d.mts → index-BSH2bloA.d.mts} +77 -3
- package/dist/index-CG712jzV.d.cts +1286 -0
- package/dist/index.cjs +188 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +24 -25
- package/dist/layered/index.cjs +38 -0
- package/dist/layered/index.d.cts +2 -0
- package/dist/layered/index.d.mts +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-COIyXvwb.mjs → layered-Bz2nWmVp.mjs} +2298 -45
- package/dist/layered-Cf9Rn8l7.cjs +12061 -0
- package/dist/layout-engine-BNex9jxg.cjs +712 -0
- package/dist/{spore-BeKUA1Mb.mjs → layout-engine-WbC0YjjT.mjs} +159 -5
- package/dist/public-types-B8rAALJq.d.mts +109 -0
- package/dist/public-types-CVB0hXGk.d.cts +109 -0
- package/dist/worker-client-D3HMK33r.cjs +98 -0
- package/dist/worker-client-DAT_z2OR.d.mts +19 -0
- package/dist/worker-client-DVhklF6A.d.cts +19 -0
- package/dist/worker-client-DkwS4sHx.mjs +92 -0
- package/package.json +39 -9
- package/src/authoring/coordinates.ts +106 -0
- package/src/authoring/partial.ts +584 -0
- package/src/authoring/routing.ts +211 -0
- package/src/box.ts +22 -0
- package/src/constraints.ts +222 -0
- package/src/elkjs/index.ts +100 -37
- package/src/elkjs/main.ts +19 -0
- package/src/elkjs/public-types.ts +125 -0
- package/src/elkjs/worker-api.ts +29 -0
- package/src/elkjs/worker-client.ts +138 -0
- package/src/elkjs/worker.ts +117 -0
- package/src/index.ts +13 -0
- package/src/internal/elkjs-compatibility.ts +17 -0
- package/src/internal/layout-engine.ts +126 -0
- package/src/layered/index.ts +31 -3
- package/src/layered/strategies.ts +68 -3
- package/src/layout.ts +35 -28
- package/src/packing.ts +22 -0
- package/src/random.ts +75 -3
- package/src/types.ts +17 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getNodeSize } from "@statelyai/graph/layout";
|
|
2
|
+
|
|
1
3
|
//#region src/errors.ts
|
|
2
4
|
var LayoutError = class extends Error {
|
|
3
5
|
constructor(message, code) {
|
|
@@ -2227,15 +2229,15 @@ function implicitEdgeEndpoints(input, placement, orientation) {
|
|
|
2227
2229
|
const hypernode = nodeSettings?.hypernode;
|
|
2228
2230
|
const unzipping = (input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING";
|
|
2229
2231
|
const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
2230
|
-
const interactiveTargetOrder = crossingStrategy === "INTERACTIVE" && entries.every(({ endpoint }) => endpoint === "target") && entries.some(({ edge }) => edge.sourceId.startsWith("__layout_dummy:"));
|
|
2231
|
-
const verticalNoneTargetOrder = !horizontal && crossingStrategy === "NONE" && input.settings["layering.strategy"] === "STRETCH_WIDTH" && entries.every(({ endpoint }) => endpoint === "target") && !input.graph.edges.some(({ sourceId }) => sourceId === nodeId) && entries.some(({ edge }) => edge.sourceId.startsWith("__layout_dummy:"));
|
|
2232
|
-
const interactiveLeftSourceOrder = crossingStrategy === "INTERACTIVE" && input.direction === "left" && entries.length >= 3 && entries.every(({ endpoint }) => endpoint === "source") ? [...entries].sort((left, right) => {
|
|
2232
|
+
const interactiveTargetOrder = crossingStrategy === "INTERACTIVE" && entries.every(({ endpoint: endpoint$1 }) => endpoint$1 === "target") && entries.some(({ edge }) => edge.sourceId.startsWith("__layout_dummy:"));
|
|
2233
|
+
const verticalNoneTargetOrder = !horizontal && crossingStrategy === "NONE" && input.settings["layering.strategy"] === "STRETCH_WIDTH" && entries.every(({ endpoint: endpoint$1 }) => endpoint$1 === "target") && !input.graph.edges.some(({ sourceId }) => sourceId === nodeId) && entries.some(({ edge }) => edge.sourceId.startsWith("__layout_dummy:"));
|
|
2234
|
+
const interactiveLeftSourceOrder = crossingStrategy === "INTERACTIVE" && input.direction === "left" && entries.length >= 3 && entries.every(({ endpoint: endpoint$1 }) => endpoint$1 === "source") ? [...entries].sort((left, right) => {
|
|
2233
2235
|
const leftRect = placement.rectByNodeId.get(left.edge.targetId);
|
|
2234
2236
|
const rightRect = placement.rectByNodeId.get(right.edge.targetId);
|
|
2235
2237
|
const leftCross = (leftRect?.y ?? 0) + (leftRect?.height ?? 0) / 2;
|
|
2236
2238
|
return (rightRect?.y ?? 0) + (rightRect?.height ?? 0) / 2 - leftCross || (edgeModelOrder.get(left.edge.id) ?? 0) - (edgeModelOrder.get(right.edge.id) ?? 0);
|
|
2237
2239
|
}).map(({ edge }) => edge.id) : void 0;
|
|
2238
|
-
const interactiveForwardLongEdgeSource = input.direction !== "left" && crossingStrategy === "INTERACTIVE" && entries.every(({ endpoint }) => endpoint === "source") && entries.some(({ edge }) => edge.targetId.startsWith("__layout_dummy:"));
|
|
2240
|
+
const interactiveForwardLongEdgeSource = input.direction !== "left" && crossingStrategy === "INTERACTIVE" && entries.every(({ endpoint: endpoint$1 }) => endpoint$1 === "source") && entries.some(({ edge }) => edge.targetId.startsWith("__layout_dummy:"));
|
|
2239
2241
|
const layerOrdered = unzipping || interactiveTargetOrder || verticalNoneTargetOrder || input.settings.hierarchyHandling !== "INCLUDE_CHILDREN" && (crossingStrategy === "LAYER_SWEEP" || crossingStrategy === "MEDIAN_LAYER_SWEEP");
|
|
2240
2242
|
entries.sort((left, right) => {
|
|
2241
2243
|
if (interactiveLeftSourceOrder) {
|
|
@@ -2288,8 +2290,8 @@ function implicitEdgeEndpoints(input, placement, orientation) {
|
|
|
2288
2290
|
const constraints = nodeSettings?.portConstraints;
|
|
2289
2291
|
const portSpacing = Number(input.settings["spacing.portPort"] ?? 10);
|
|
2290
2292
|
const fixedWrapAnchor = nodeId.startsWith("__layout_dummy:wrap:");
|
|
2291
|
-
entries.forEach(({ edge, endpoint }, index) => {
|
|
2292
|
-
const ordinal = endpoint === "target" && input.settings["crossingMinimization.strategy"] !== "INTERACTIVE" && !layerOrdered && !(input.settings.directionCongruency === "ROTATION" && (input.direction === "down" || input.direction === "left")) ? entries.length - index : index + 1;
|
|
2293
|
+
entries.forEach(({ edge, endpoint: endpoint$1 }, index) => {
|
|
2294
|
+
const ordinal = endpoint$1 === "target" && input.settings["crossingMinimization.strategy"] !== "INTERACTIVE" && !layerOrdered && !(input.settings.directionCongruency === "ROTATION" && (input.direction === "down" || input.direction === "left")) ? entries.length - index : index + 1;
|
|
2293
2295
|
const orderedIndex = ordinal - 1;
|
|
2294
2296
|
const crossPosition = mergeEdges || hypernode === true ? axisSize / 2 : constraints === "FIXED_RATIO" || constraints === "FIXED_POS" ? 0 : alignment === "BEGIN" ? axisStart + orderedIndex * portSpacing : alignment === "END" ? axisStart + availableAxisSize - (entries.length - orderedIndex - 1) * portSpacing : alignment === "CENTER" ? axisStart + (availableAxisSize - (entries.length - 1) * portSpacing) / 2 + orderedIndex * portSpacing : alignment === "JUSTIFIED" ? entries.length === 1 ? axisStart + availableAxisSize / 2 : axisStart + orderedIndex * availableAxisSize / (entries.length - 1) : axisStart + availableAxisSize * ordinal / (entries.length + 1);
|
|
2295
2297
|
let point = horizontal ? {
|
|
@@ -2310,7 +2312,7 @@ function implicitEdgeEndpoints(input, placement, orientation) {
|
|
|
2310
2312
|
source: point,
|
|
2311
2313
|
target: point
|
|
2312
2314
|
};
|
|
2313
|
-
pair[endpoint] = point;
|
|
2315
|
+
pair[endpoint$1] = point;
|
|
2314
2316
|
result.set(edge.id, pair);
|
|
2315
2317
|
});
|
|
2316
2318
|
}
|
|
@@ -2494,7 +2496,40 @@ function routeEdges(style) {
|
|
|
2494
2496
|
}
|
|
2495
2497
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
2496
2498
|
}
|
|
2499
|
+
const increasing = input.direction === "right" || input.direction === "down";
|
|
2500
|
+
const flowPortProtrusion = (edge, endpoint$1) => {
|
|
2501
|
+
const node = nodeById.get(endpoint$1 === "source" ? edge.sourceId : edge.targetId);
|
|
2502
|
+
const portName = endpoint$1 === "source" ? edge.sourcePort : edge.targetPort;
|
|
2503
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
2504
|
+
if (!node || !port) return 0;
|
|
2505
|
+
return input.portSettings?.(port, node)?.["port.side"] === (horizontal ? endpoint$1 === "source" ? input.direction === "right" ? "EAST" : "WEST" : input.direction === "right" ? "WEST" : "EAST" : endpoint$1 === "source" ? input.direction === "down" ? "SOUTH" : "NORTH" : input.direction === "down" ? "NORTH" : "SOUTH") ? horizontal ? port.width ?? 0 : port.height ?? 0 : 0;
|
|
2506
|
+
};
|
|
2507
|
+
const adjacentNodeIds = /* @__PURE__ */ new Map();
|
|
2508
|
+
for (const edge of input.graph.edges) {
|
|
2509
|
+
const sourceNeighbors = adjacentNodeIds.get(edge.sourceId) ?? /* @__PURE__ */ new Set();
|
|
2510
|
+
sourceNeighbors.add(edge.targetId);
|
|
2511
|
+
adjacentNodeIds.set(edge.sourceId, sourceNeighbors);
|
|
2512
|
+
const targetNeighbors = adjacentNodeIds.get(edge.targetId) ?? /* @__PURE__ */ new Set();
|
|
2513
|
+
targetNeighbors.add(edge.sourceId);
|
|
2514
|
+
adjacentNodeIds.set(edge.targetId, targetNeighbors);
|
|
2515
|
+
}
|
|
2516
|
+
const feedbackComponentNodeIds = /* @__PURE__ */ new Set();
|
|
2517
|
+
for (const edge of input.graph.edges) {
|
|
2518
|
+
const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
|
|
2519
|
+
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
2520
|
+
if (sourceLayer === void 0 || targetLayer === void 0) continue;
|
|
2521
|
+
if (sourceLayer === targetLayer) continue;
|
|
2522
|
+
if (increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer) continue;
|
|
2523
|
+
const pending = [edge.sourceId, edge.targetId];
|
|
2524
|
+
while (pending.length > 0) {
|
|
2525
|
+
const nodeId = pending.pop();
|
|
2526
|
+
if (feedbackComponentNodeIds.has(nodeId)) continue;
|
|
2527
|
+
feedbackComponentNodeIds.add(nodeId);
|
|
2528
|
+
pending.push(...adjacentNodeIds.get(nodeId) ?? []);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2497
2531
|
const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
|
|
2532
|
+
const compactLabelCorridorGaps = /* @__PURE__ */ new Set();
|
|
2498
2533
|
for (const edge of input.graph.edges) {
|
|
2499
2534
|
const labelFlowSize = horizontal ? edge.width ?? 0 : edge.height ?? 0;
|
|
2500
2535
|
if (labelFlowSize <= 0) continue;
|
|
@@ -2502,8 +2537,14 @@ function routeEdges(style) {
|
|
|
2502
2537
|
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
2503
2538
|
if (sourceLayer === void 0 || targetLayer === void 0) continue;
|
|
2504
2539
|
if (Math.abs(sourceLayer - targetLayer) !== 1) continue;
|
|
2505
|
-
const
|
|
2540
|
+
const placement$1 = input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER";
|
|
2541
|
+
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
2542
|
+
const requiredGap = labelFlowSize + Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) + Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing);
|
|
2506
2543
|
const gap = Math.min(sourceLayer, targetLayer);
|
|
2544
|
+
const availableGap = flowLayers[gap + 1].start - flowLayers[gap].end;
|
|
2545
|
+
const compactCorridor = placement$1 === "CENTER" && (increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer) && !feedbackComponentNodeIds.has(edge.sourceId);
|
|
2546
|
+
if (compactCorridor) compactLabelCorridorGaps.add(gap);
|
|
2547
|
+
const extra = compactCorridor ? Math.max(0, requiredGap - availableGap) : placement$1 === "CENTER" ? labelFlowSize + input.spacing.layer : labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
2507
2548
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
2508
2549
|
}
|
|
2509
2550
|
let labelShift = 0;
|
|
@@ -2749,7 +2790,7 @@ function routeEdges(style) {
|
|
|
2749
2790
|
const slots = slotsByGap[layerNo] ?? 0;
|
|
2750
2791
|
const routesNearTarget = candidatesByGap[layerNo]?.some((candidate) => hasZeroFixedSideTarget(candidate.edge) && candidatesByGap[layerNo].filter(({ edge }) => edge.targetId === candidate.edge.targetId).length > candidatesByGap[layerNo].filter(({ edge }) => edge.sourceId === candidate.edge.sourceId).length);
|
|
2751
2792
|
const preservedGap = (existingGapByLayer[layerNo] ?? input.spacing.layer) - (routesNearTarget ? edgeEdgeSpacing : 0);
|
|
2752
|
-
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 || [...selfLoopsByNodeId].some(([id, loops]) => (flowLayerByNodeId.get(id) === layerNo || flowLayerByNodeId.get(id) === layerNo + 1) && loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2793
|
+
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 || compactLabelCorridorGaps.has(layerNo) || [...selfLoopsByNodeId].some(([id, loops]) => (flowLayerByNodeId.get(id) === layerNo || flowLayerByNodeId.get(id) === layerNo + 1) && loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2753
2794
|
nextStart = bounds.end + gapSpacing;
|
|
2754
2795
|
}
|
|
2755
2796
|
implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
@@ -3779,6 +3820,2206 @@ function getPolylineMidpoint(points) {
|
|
|
3779
3820
|
};
|
|
3780
3821
|
}
|
|
3781
3822
|
|
|
3823
|
+
//#endregion
|
|
3824
|
+
//#region src/authoring/coordinates.ts
|
|
3825
|
+
/** Native compound nodes and sibling edges are parent-relative; crossing edges are world-relative. */
|
|
3826
|
+
function frames(graph) {
|
|
3827
|
+
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
3828
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
3829
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
3830
|
+
function offset(id) {
|
|
3831
|
+
const cached = offsets.get(id);
|
|
3832
|
+
if (cached) return cached;
|
|
3833
|
+
if (visiting.has(id)) throw new LayoutError(`Cyclic parent hierarchy at ${id}`, "INVALID_GRAPH");
|
|
3834
|
+
visiting.add(id);
|
|
3835
|
+
const parentId = nodes.get(id)?.parentId;
|
|
3836
|
+
if (parentId == null) {
|
|
3837
|
+
const origin = {
|
|
3838
|
+
x: 0,
|
|
3839
|
+
y: 0
|
|
3840
|
+
};
|
|
3841
|
+
visiting.delete(id);
|
|
3842
|
+
offsets.set(id, origin);
|
|
3843
|
+
return origin;
|
|
3844
|
+
}
|
|
3845
|
+
const parent = nodes.get(parentId);
|
|
3846
|
+
const ancestor = offset(parentId);
|
|
3847
|
+
const result = {
|
|
3848
|
+
x: ancestor.x + parent.x,
|
|
3849
|
+
y: ancestor.y + parent.y
|
|
3850
|
+
};
|
|
3851
|
+
visiting.delete(id);
|
|
3852
|
+
offsets.set(id, result);
|
|
3853
|
+
return result;
|
|
3854
|
+
}
|
|
3855
|
+
for (const node of graph.nodes) offsets.set(node.id, offset(node.id));
|
|
3856
|
+
return {
|
|
3857
|
+
nodes: offsets,
|
|
3858
|
+
edges: new Map(graph.edges.map((edge) => {
|
|
3859
|
+
const source = nodes.get(edge.sourceId);
|
|
3860
|
+
const target = nodes.get(edge.targetId);
|
|
3861
|
+
return [edge.id, (source.parentId ?? null) === (target.parentId ?? null) ? offset(source.id) : {
|
|
3862
|
+
x: 0,
|
|
3863
|
+
y: 0
|
|
3864
|
+
}];
|
|
3865
|
+
}))
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
function worldGeometry(graph) {
|
|
3869
|
+
const offsets = frames(graph);
|
|
3870
|
+
return {
|
|
3871
|
+
...graph,
|
|
3872
|
+
nodes: graph.nodes.map((n) => {
|
|
3873
|
+
const p = offsets.nodes.get(n.id);
|
|
3874
|
+
return p.x === 0 && p.y === 0 ? n : {
|
|
3875
|
+
...n,
|
|
3876
|
+
x: n.x + p.x,
|
|
3877
|
+
y: n.y + p.y
|
|
3878
|
+
};
|
|
3879
|
+
}),
|
|
3880
|
+
edges: graph.edges.map((e) => {
|
|
3881
|
+
const p = offsets.edges.get(e.id);
|
|
3882
|
+
return p.x === 0 && p.y === 0 ? e : {
|
|
3883
|
+
...e,
|
|
3884
|
+
x: e.x + p.x,
|
|
3885
|
+
y: e.y + p.y,
|
|
3886
|
+
...e.points ? { points: e.points.map((v) => ({
|
|
3887
|
+
x: v.x + p.x,
|
|
3888
|
+
y: v.y + p.y
|
|
3889
|
+
})) } : {}
|
|
3890
|
+
};
|
|
3891
|
+
})
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
/** Ancestor frames stay fixed during authoring. Preserve untouched coordinates exactly. */
|
|
3895
|
+
function nativeGeometry(graph, original) {
|
|
3896
|
+
const offsets = frames(original);
|
|
3897
|
+
const world = worldGeometry(original);
|
|
3898
|
+
const originalNodes = new Map(original.nodes.map((n) => [n.id, n]));
|
|
3899
|
+
const originalEdges = new Map(original.edges.map((e) => [e.id, e]));
|
|
3900
|
+
const worldNodes = new Map(world.nodes.map((n) => [n.id, n]));
|
|
3901
|
+
const worldEdges = new Map(world.edges.map((e) => [e.id, e]));
|
|
3902
|
+
return {
|
|
3903
|
+
...graph,
|
|
3904
|
+
nodes: graph.nodes.map((n) => {
|
|
3905
|
+
const p = offsets.nodes.get(n.id), before = originalNodes.get(n.id), prior = worldNodes.get(n.id);
|
|
3906
|
+
return {
|
|
3907
|
+
...n,
|
|
3908
|
+
x: n.x === prior.x ? before.x : n.x - p.x,
|
|
3909
|
+
y: n.y === prior.y ? before.y : n.y - p.y
|
|
3910
|
+
};
|
|
3911
|
+
}),
|
|
3912
|
+
edges: graph.edges.map((e) => {
|
|
3913
|
+
const p = offsets.edges.get(e.id), before = originalEdges.get(e.id), prior = worldEdges.get(e.id);
|
|
3914
|
+
const points = JSON.stringify(e.points) === JSON.stringify(prior.points) ? before.points : e.points?.map((v) => ({
|
|
3915
|
+
x: v.x - p.x,
|
|
3916
|
+
y: v.y - p.y
|
|
3917
|
+
}));
|
|
3918
|
+
return {
|
|
3919
|
+
...e,
|
|
3920
|
+
x: e.x === prior.x ? before.x : e.x - p.x,
|
|
3921
|
+
y: e.y === prior.y ? before.y : e.y - p.y,
|
|
3922
|
+
...points === void 0 ? {} : { points }
|
|
3923
|
+
};
|
|
3924
|
+
})
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
//#endregion
|
|
3929
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/maptype.js
|
|
3930
|
+
function createMap() {
|
|
3931
|
+
return new IndexedMap();
|
|
3932
|
+
}
|
|
3933
|
+
var IndexedMap = class IndexedMap {
|
|
3934
|
+
index = {};
|
|
3935
|
+
array = [];
|
|
3936
|
+
/**
|
|
3937
|
+
* Returns the number of items in the array.
|
|
3938
|
+
*/
|
|
3939
|
+
size() {
|
|
3940
|
+
return this.array.length;
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Returns true if the array is empty.
|
|
3944
|
+
*/
|
|
3945
|
+
empty() {
|
|
3946
|
+
return this.array.length === 0;
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Returns the item at the given array index.
|
|
3950
|
+
*
|
|
3951
|
+
* @param index The integer index of the desired item.
|
|
3952
|
+
*/
|
|
3953
|
+
itemAt(index) {
|
|
3954
|
+
return this.array[index];
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Returns true if the key is in the array, false otherwise.
|
|
3958
|
+
*
|
|
3959
|
+
* @param key The key to locate in the array.
|
|
3960
|
+
*/
|
|
3961
|
+
contains(key) {
|
|
3962
|
+
return this.index[key.id()] !== void 0;
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* Returns the pair associated with the given key, or undefined.
|
|
3966
|
+
*
|
|
3967
|
+
* @param key The key to locate in the array.
|
|
3968
|
+
*/
|
|
3969
|
+
find(key) {
|
|
3970
|
+
const i = this.index[key.id()];
|
|
3971
|
+
return i === void 0 ? void 0 : this.array[i];
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* Returns the pair associated with the key if it exists.
|
|
3975
|
+
*
|
|
3976
|
+
* If the key does not exist, a new pair will be created and
|
|
3977
|
+
* inserted using the value created by the given factory.
|
|
3978
|
+
*
|
|
3979
|
+
* @param key The key to locate in the array.
|
|
3980
|
+
* @param factory The function which creates the default value.
|
|
3981
|
+
*/
|
|
3982
|
+
setDefault(key, factory) {
|
|
3983
|
+
const i = this.index[key.id()];
|
|
3984
|
+
if (i === void 0) {
|
|
3985
|
+
const pair = new Pair(key, factory());
|
|
3986
|
+
this.index[key.id()] = this.array.length;
|
|
3987
|
+
this.array.push(pair);
|
|
3988
|
+
return pair;
|
|
3989
|
+
} else return this.array[i];
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Insert the pair into the array and return the pair.
|
|
3993
|
+
*
|
|
3994
|
+
* This will overwrite any existing entry in the array.
|
|
3995
|
+
*
|
|
3996
|
+
* @param key The key portion of the pair.
|
|
3997
|
+
* @param value The value portion of the pair.
|
|
3998
|
+
*/
|
|
3999
|
+
insert(key, value) {
|
|
4000
|
+
const pair = new Pair(key, value);
|
|
4001
|
+
const i = this.index[key.id()];
|
|
4002
|
+
if (i === void 0) {
|
|
4003
|
+
this.index[key.id()] = this.array.length;
|
|
4004
|
+
this.array.push(pair);
|
|
4005
|
+
} else this.array[i] = pair;
|
|
4006
|
+
return pair;
|
|
4007
|
+
}
|
|
4008
|
+
/**
|
|
4009
|
+
* Removes and returns the pair for the given key, or undefined.
|
|
4010
|
+
*
|
|
4011
|
+
* @param key The key to remove from the map.
|
|
4012
|
+
*/
|
|
4013
|
+
erase(key) {
|
|
4014
|
+
const i = this.index[key.id()];
|
|
4015
|
+
if (i === void 0) return;
|
|
4016
|
+
this.index[key.id()] = void 0;
|
|
4017
|
+
const pair = this.array[i];
|
|
4018
|
+
const last = this.array.pop();
|
|
4019
|
+
if (pair !== last) {
|
|
4020
|
+
this.array[i] = last;
|
|
4021
|
+
this.index[last.first.id()] = i;
|
|
4022
|
+
}
|
|
4023
|
+
return pair;
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Create a copy of this associative array.
|
|
4027
|
+
*/
|
|
4028
|
+
copy() {
|
|
4029
|
+
const copy = new IndexedMap();
|
|
4030
|
+
for (let i = 0; i < this.array.length; i++) {
|
|
4031
|
+
const pair = this.array[i].copy();
|
|
4032
|
+
copy.array[i] = pair;
|
|
4033
|
+
copy.index[pair.first.id()] = i;
|
|
4034
|
+
}
|
|
4035
|
+
return copy;
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
/**
|
|
4039
|
+
* A class which defines a generic pair object.
|
|
4040
|
+
* @private
|
|
4041
|
+
*/
|
|
4042
|
+
var Pair = class Pair {
|
|
4043
|
+
first;
|
|
4044
|
+
second;
|
|
4045
|
+
/**
|
|
4046
|
+
* Construct a new Pair object.
|
|
4047
|
+
*
|
|
4048
|
+
* @param first The first item of the pair.
|
|
4049
|
+
* @param second The second item of the pair.
|
|
4050
|
+
*/
|
|
4051
|
+
constructor(first, second) {
|
|
4052
|
+
this.first = first;
|
|
4053
|
+
this.second = second;
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Create a copy of the pair.
|
|
4057
|
+
*/
|
|
4058
|
+
copy() {
|
|
4059
|
+
return new Pair(this.first, this.second);
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
|
|
4063
|
+
//#endregion
|
|
4064
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/variable.js
|
|
4065
|
+
/**
|
|
4066
|
+
* The primary user constraint variable.
|
|
4067
|
+
*
|
|
4068
|
+
* @class
|
|
4069
|
+
* @param {String} [name=""] The name to associated with the variable.
|
|
4070
|
+
*/
|
|
4071
|
+
var Variable = class {
|
|
4072
|
+
constructor(name = "") {
|
|
4073
|
+
this._name = name;
|
|
4074
|
+
}
|
|
4075
|
+
/**
|
|
4076
|
+
* Returns the unique id number of the variable.
|
|
4077
|
+
* @private
|
|
4078
|
+
*/
|
|
4079
|
+
id() {
|
|
4080
|
+
return this._id;
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Returns the name of the variable.
|
|
4084
|
+
*
|
|
4085
|
+
* @return {String} name of the variable
|
|
4086
|
+
*/
|
|
4087
|
+
name() {
|
|
4088
|
+
return this._name;
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Set the name of the variable.
|
|
4092
|
+
*
|
|
4093
|
+
* @param {String} name Name of the variable
|
|
4094
|
+
*/
|
|
4095
|
+
setName(name) {
|
|
4096
|
+
this._name = name;
|
|
4097
|
+
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Returns the user context object of the variable.
|
|
4100
|
+
* @private
|
|
4101
|
+
*/
|
|
4102
|
+
context() {
|
|
4103
|
+
return this._context;
|
|
4104
|
+
}
|
|
4105
|
+
/**
|
|
4106
|
+
* Set the user context object of the variable.
|
|
4107
|
+
* @private
|
|
4108
|
+
*/
|
|
4109
|
+
setContext(context) {
|
|
4110
|
+
this._context = context;
|
|
4111
|
+
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Returns the value of the variable.
|
|
4114
|
+
*
|
|
4115
|
+
* @return {Number} Calculated value
|
|
4116
|
+
*/
|
|
4117
|
+
value() {
|
|
4118
|
+
return this._value;
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Set the value of the variable.
|
|
4122
|
+
* @private
|
|
4123
|
+
*/
|
|
4124
|
+
setValue(value) {
|
|
4125
|
+
this._value = value;
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Creates a new Expression by adding a number, variable or expression
|
|
4129
|
+
* to the variable.
|
|
4130
|
+
*
|
|
4131
|
+
* @param {Number|Variable|Expression} value Value to add.
|
|
4132
|
+
* @return {Expression} expression
|
|
4133
|
+
*/
|
|
4134
|
+
plus(value) {
|
|
4135
|
+
return new Expression(this, value);
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Creates a new Expression by substracting a number, variable or expression
|
|
4139
|
+
* from the variable.
|
|
4140
|
+
*
|
|
4141
|
+
* @param {Number|Variable|Expression} value Value to substract.
|
|
4142
|
+
* @return {Expression} expression
|
|
4143
|
+
*/
|
|
4144
|
+
minus(value) {
|
|
4145
|
+
return new Expression(this, typeof value === "number" ? -value : [-1, value]);
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Creates a new Expression by multiplying with a fixed number.
|
|
4149
|
+
*
|
|
4150
|
+
* @param {Number} coefficient Coefficient to multiply with.
|
|
4151
|
+
* @return {Expression} expression
|
|
4152
|
+
*/
|
|
4153
|
+
multiply(coefficient) {
|
|
4154
|
+
return new Expression([coefficient, this]);
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Creates a new Expression by dividing with a fixed number.
|
|
4158
|
+
*
|
|
4159
|
+
* @param {Number} coefficient Coefficient to divide by.
|
|
4160
|
+
* @return {Expression} expression
|
|
4161
|
+
*/
|
|
4162
|
+
divide(coefficient) {
|
|
4163
|
+
return new Expression([1 / coefficient, this]);
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Returns the JSON representation of the variable.
|
|
4167
|
+
* @private
|
|
4168
|
+
*/
|
|
4169
|
+
toJSON() {
|
|
4170
|
+
return {
|
|
4171
|
+
name: this._name,
|
|
4172
|
+
value: this._value
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
toString() {
|
|
4176
|
+
return this._context + "[" + this._name + ":" + this._value + "]";
|
|
4177
|
+
}
|
|
4178
|
+
_name;
|
|
4179
|
+
_value = 0;
|
|
4180
|
+
_context = null;
|
|
4181
|
+
_id = VarId++;
|
|
4182
|
+
};
|
|
4183
|
+
/**
|
|
4184
|
+
* The internal variable id counter.
|
|
4185
|
+
* @private
|
|
4186
|
+
*/
|
|
4187
|
+
let VarId = 0;
|
|
4188
|
+
|
|
4189
|
+
//#endregion
|
|
4190
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/expression.js
|
|
4191
|
+
/**
|
|
4192
|
+
* An expression of variable terms and a constant.
|
|
4193
|
+
*
|
|
4194
|
+
* The constructor accepts an arbitrary number of parameters,
|
|
4195
|
+
* each of which must be one of the following types:
|
|
4196
|
+
* - number
|
|
4197
|
+
* - Variable
|
|
4198
|
+
* - Expression
|
|
4199
|
+
* - 2-tuple of [number, Variable|Expression]
|
|
4200
|
+
*
|
|
4201
|
+
* The parameters are summed. The tuples are multiplied.
|
|
4202
|
+
*
|
|
4203
|
+
* @class
|
|
4204
|
+
* @param {...(number|Variable|Expression|Array)} args
|
|
4205
|
+
*/
|
|
4206
|
+
var Expression = class Expression {
|
|
4207
|
+
constructor() {
|
|
4208
|
+
let parsed = parseArgs(arguments);
|
|
4209
|
+
this._terms = parsed.terms;
|
|
4210
|
+
this._constant = parsed.constant;
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* Returns the mapping of terms in the expression.
|
|
4214
|
+
*
|
|
4215
|
+
* This *must* be treated as const.
|
|
4216
|
+
* @private
|
|
4217
|
+
*/
|
|
4218
|
+
terms() {
|
|
4219
|
+
return this._terms;
|
|
4220
|
+
}
|
|
4221
|
+
/**
|
|
4222
|
+
* Returns the constant of the expression.
|
|
4223
|
+
* @private
|
|
4224
|
+
*/
|
|
4225
|
+
constant() {
|
|
4226
|
+
return this._constant;
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Returns the computed value of the expression.
|
|
4230
|
+
*
|
|
4231
|
+
* @private
|
|
4232
|
+
* @return {Number} computed value of the expression
|
|
4233
|
+
*/
|
|
4234
|
+
value() {
|
|
4235
|
+
let result = this._constant;
|
|
4236
|
+
for (let i = 0, n = this._terms.size(); i < n; i++) {
|
|
4237
|
+
let pair = this._terms.itemAt(i);
|
|
4238
|
+
result += pair.first.value() * pair.second;
|
|
4239
|
+
}
|
|
4240
|
+
return result;
|
|
4241
|
+
}
|
|
4242
|
+
/**
|
|
4243
|
+
* Creates a new Expression by adding a number, variable or expression
|
|
4244
|
+
* to the expression.
|
|
4245
|
+
*
|
|
4246
|
+
* @param {Number|Variable|Expression} value Value to add.
|
|
4247
|
+
* @return {Expression} expression
|
|
4248
|
+
*/
|
|
4249
|
+
plus(value) {
|
|
4250
|
+
return new Expression(this, value);
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* Creates a new Expression by substracting a number, variable or expression
|
|
4254
|
+
* from the expression.
|
|
4255
|
+
*
|
|
4256
|
+
* @param {Number|Variable|Expression} value Value to substract.
|
|
4257
|
+
* @return {Expression} expression
|
|
4258
|
+
*/
|
|
4259
|
+
minus(value) {
|
|
4260
|
+
return new Expression(this, typeof value === "number" ? -value : [-1, value]);
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Creates a new Expression by multiplying with a fixed number.
|
|
4264
|
+
*
|
|
4265
|
+
* @param {Number} coefficient Coefficient to multiply with.
|
|
4266
|
+
* @return {Expression} expression
|
|
4267
|
+
*/
|
|
4268
|
+
multiply(coefficient) {
|
|
4269
|
+
return new Expression([coefficient, this]);
|
|
4270
|
+
}
|
|
4271
|
+
/**
|
|
4272
|
+
* Creates a new Expression by dividing with a fixed number.
|
|
4273
|
+
*
|
|
4274
|
+
* @param {Number} coefficient Coefficient to divide by.
|
|
4275
|
+
* @return {Expression} expression
|
|
4276
|
+
*/
|
|
4277
|
+
divide(coefficient) {
|
|
4278
|
+
return new Expression([1 / coefficient, this]);
|
|
4279
|
+
}
|
|
4280
|
+
isConstant() {
|
|
4281
|
+
return this._terms.size() == 0;
|
|
4282
|
+
}
|
|
4283
|
+
toString() {
|
|
4284
|
+
let result = this._terms.array.map(function(pair) {
|
|
4285
|
+
return pair.second + "*" + pair.first.toString();
|
|
4286
|
+
}).join(" + ");
|
|
4287
|
+
if (!this.isConstant() && this._constant !== 0) result += " + ";
|
|
4288
|
+
result += this._constant;
|
|
4289
|
+
return result;
|
|
4290
|
+
}
|
|
4291
|
+
_terms;
|
|
4292
|
+
_constant;
|
|
4293
|
+
};
|
|
4294
|
+
/**
|
|
4295
|
+
* An internal argument parsing function.
|
|
4296
|
+
* @private
|
|
4297
|
+
*/
|
|
4298
|
+
function parseArgs(args) {
|
|
4299
|
+
let constant = 0;
|
|
4300
|
+
let factory = () => 0;
|
|
4301
|
+
let terms = createMap();
|
|
4302
|
+
for (let i = 0, n = args.length; i < n; ++i) {
|
|
4303
|
+
let item = args[i];
|
|
4304
|
+
if (typeof item === "number") constant += item;
|
|
4305
|
+
else if (item instanceof Variable) terms.setDefault(item, factory).second += 1;
|
|
4306
|
+
else if (item instanceof Expression) {
|
|
4307
|
+
constant += item.constant();
|
|
4308
|
+
let terms2 = item.terms();
|
|
4309
|
+
for (let j = 0, k = terms2.size(); j < k; j++) {
|
|
4310
|
+
let termPair = terms2.itemAt(j);
|
|
4311
|
+
terms.setDefault(termPair.first, factory).second += termPair.second;
|
|
4312
|
+
}
|
|
4313
|
+
} else if (item instanceof Array) {
|
|
4314
|
+
if (item.length !== 2) throw new Error("array must have length 2");
|
|
4315
|
+
let value = item[0];
|
|
4316
|
+
let value2 = item[1];
|
|
4317
|
+
if (typeof value !== "number") throw new Error("array item 0 must be a number");
|
|
4318
|
+
if (value2 instanceof Variable) terms.setDefault(value2, factory).second += value;
|
|
4319
|
+
else if (value2 instanceof Expression) {
|
|
4320
|
+
constant += value2.constant() * value;
|
|
4321
|
+
let terms2 = value2.terms();
|
|
4322
|
+
for (let j = 0, k = terms2.size(); j < k; j++) {
|
|
4323
|
+
let termPair = terms2.itemAt(j);
|
|
4324
|
+
terms.setDefault(termPair.first, factory).second += termPair.second * value;
|
|
4325
|
+
}
|
|
4326
|
+
} else throw new Error("array item 1 must be a variable or expression");
|
|
4327
|
+
} else throw new Error("invalid Expression argument: " + item);
|
|
4328
|
+
}
|
|
4329
|
+
return {
|
|
4330
|
+
terms,
|
|
4331
|
+
constant
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
//#endregion
|
|
4336
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/strength.js
|
|
4337
|
+
/**
|
|
4338
|
+
* @class Strength
|
|
4339
|
+
*/
|
|
4340
|
+
var Strength = class Strength {
|
|
4341
|
+
/**
|
|
4342
|
+
* Create a new symbolic strength.
|
|
4343
|
+
*
|
|
4344
|
+
* @param a strong
|
|
4345
|
+
* @param b medium
|
|
4346
|
+
* @param c weak
|
|
4347
|
+
* @param [w] weight
|
|
4348
|
+
* @return strength
|
|
4349
|
+
*/
|
|
4350
|
+
static create(a, b, c$1, w = 1) {
|
|
4351
|
+
let result = 0;
|
|
4352
|
+
result += Math.max(0, Math.min(1e3, a * w)) * 1e6;
|
|
4353
|
+
result += Math.max(0, Math.min(1e3, b * w)) * 1e3;
|
|
4354
|
+
result += Math.max(0, Math.min(1e3, c$1 * w));
|
|
4355
|
+
return result;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* The 'required' symbolic strength.
|
|
4359
|
+
*/
|
|
4360
|
+
static required = Strength.create(1e3, 1e3, 1e3);
|
|
4361
|
+
/**
|
|
4362
|
+
* The 'strong' symbolic strength.
|
|
4363
|
+
*/
|
|
4364
|
+
static strong = Strength.create(1, 0, 0);
|
|
4365
|
+
/**
|
|
4366
|
+
* The 'medium' symbolic strength.
|
|
4367
|
+
*/
|
|
4368
|
+
static medium = Strength.create(0, 1, 0);
|
|
4369
|
+
/**
|
|
4370
|
+
* The 'weak' symbolic strength.
|
|
4371
|
+
*/
|
|
4372
|
+
static weak = Strength.create(0, 0, 1);
|
|
4373
|
+
/**
|
|
4374
|
+
* Clip a symbolic strength to the allowed min and max.
|
|
4375
|
+
* @private
|
|
4376
|
+
*/
|
|
4377
|
+
static clip(value) {
|
|
4378
|
+
return Math.max(0, Math.min(Strength.required, value));
|
|
4379
|
+
}
|
|
4380
|
+
};
|
|
4381
|
+
|
|
4382
|
+
//#endregion
|
|
4383
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/constraint.js
|
|
4384
|
+
/**
|
|
4385
|
+
* An enum defining the linear constraint operators.
|
|
4386
|
+
*
|
|
4387
|
+
* |Value|Operator|Description|
|
|
4388
|
+
* |----|-----|-----|
|
|
4389
|
+
* |`Le`|<=|Less than equal|
|
|
4390
|
+
* |`Ge`|>=|Greater than equal|
|
|
4391
|
+
* |`Eq`|==|Equal|
|
|
4392
|
+
*
|
|
4393
|
+
* @enum {Number}
|
|
4394
|
+
*/
|
|
4395
|
+
var Operator;
|
|
4396
|
+
(function(Operator$1) {
|
|
4397
|
+
Operator$1[Operator$1["Le"] = 0] = "Le";
|
|
4398
|
+
Operator$1[Operator$1["Ge"] = 1] = "Ge";
|
|
4399
|
+
Operator$1[Operator$1["Eq"] = 2] = "Eq";
|
|
4400
|
+
})(Operator || (Operator = {}));
|
|
4401
|
+
/**
|
|
4402
|
+
* A linear constraint equation.
|
|
4403
|
+
*
|
|
4404
|
+
* A constraint equation is composed of an expression, an operator,
|
|
4405
|
+
* and a strength. The RHS of the equation is implicitly zero.
|
|
4406
|
+
*
|
|
4407
|
+
* @class
|
|
4408
|
+
* @param {Expression} expression The constraint expression (LHS).
|
|
4409
|
+
* @param {Operator} operator The equation operator.
|
|
4410
|
+
* @param {Expression} [rhs] Right hand side of the expression.
|
|
4411
|
+
* @param {Number} [strength=Strength.required] The strength of the constraint.
|
|
4412
|
+
*/
|
|
4413
|
+
var Constraint = class {
|
|
4414
|
+
constructor(expression, operator, rhs, strength = Strength.required) {
|
|
4415
|
+
this._operator = operator;
|
|
4416
|
+
this._strength = Strength.clip(strength);
|
|
4417
|
+
if (rhs === void 0 && expression instanceof Expression) this._expression = expression;
|
|
4418
|
+
else this._expression = expression.minus(rhs);
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Returns the unique id number of the constraint.
|
|
4422
|
+
* @private
|
|
4423
|
+
*/
|
|
4424
|
+
id() {
|
|
4425
|
+
return this._id;
|
|
4426
|
+
}
|
|
4427
|
+
/**
|
|
4428
|
+
* Returns the expression of the constraint.
|
|
4429
|
+
*
|
|
4430
|
+
* @return {Expression} expression
|
|
4431
|
+
*/
|
|
4432
|
+
expression() {
|
|
4433
|
+
return this._expression;
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* Returns the relational operator of the constraint.
|
|
4437
|
+
*
|
|
4438
|
+
* @return {Operator} linear constraint operator
|
|
4439
|
+
*/
|
|
4440
|
+
op() {
|
|
4441
|
+
return this._operator;
|
|
4442
|
+
}
|
|
4443
|
+
/**
|
|
4444
|
+
* Returns the strength of the constraint.
|
|
4445
|
+
*
|
|
4446
|
+
* @return {Number} strength
|
|
4447
|
+
*/
|
|
4448
|
+
strength() {
|
|
4449
|
+
return this._strength;
|
|
4450
|
+
}
|
|
4451
|
+
toString() {
|
|
4452
|
+
return this._expression.toString() + " " + [
|
|
4453
|
+
"<=",
|
|
4454
|
+
">=",
|
|
4455
|
+
"="
|
|
4456
|
+
][this._operator] + " 0 (" + this._strength.toString() + ")";
|
|
4457
|
+
}
|
|
4458
|
+
_expression;
|
|
4459
|
+
_operator;
|
|
4460
|
+
_strength;
|
|
4461
|
+
_id = CnId++;
|
|
4462
|
+
};
|
|
4463
|
+
/**
|
|
4464
|
+
* The internal constraint id counter.
|
|
4465
|
+
* @private
|
|
4466
|
+
*/
|
|
4467
|
+
let CnId = 0;
|
|
4468
|
+
|
|
4469
|
+
//#endregion
|
|
4470
|
+
//#region node_modules/.pnpm/@lume+kiwi@0.4.4/node_modules/@lume/kiwi/dist/solver.js
|
|
4471
|
+
/**
|
|
4472
|
+
* The constraint solver class.
|
|
4473
|
+
*
|
|
4474
|
+
* @class
|
|
4475
|
+
*/
|
|
4476
|
+
var Solver = class {
|
|
4477
|
+
/**
|
|
4478
|
+
* @type {number} - The max number of solver iterations before an error
|
|
4479
|
+
* is thrown, in order to prevent infinite iteration. Default: `10,000`.
|
|
4480
|
+
*/
|
|
4481
|
+
maxIterations = 1e3;
|
|
4482
|
+
/**
|
|
4483
|
+
* Construct a new Solver.
|
|
4484
|
+
*/
|
|
4485
|
+
constructor() {}
|
|
4486
|
+
/**
|
|
4487
|
+
* Creates and add a constraint to the solver.
|
|
4488
|
+
*
|
|
4489
|
+
* @param {Expression|Variable} lhs Left hand side of the expression
|
|
4490
|
+
* @param {Operator} operator Operator
|
|
4491
|
+
* @param {Expression|Variable|Number} rhs Right hand side of the expression
|
|
4492
|
+
* @param {Number} [strength=Strength.required] Strength
|
|
4493
|
+
*/
|
|
4494
|
+
createConstraint(lhs, operator, rhs, strength = Strength.required) {
|
|
4495
|
+
let cn = new Constraint(lhs, operator, rhs, strength);
|
|
4496
|
+
this.addConstraint(cn);
|
|
4497
|
+
return cn;
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* Add a constraint to the solver.
|
|
4501
|
+
*
|
|
4502
|
+
* @param {Constraint} constraint Constraint to add to the solver
|
|
4503
|
+
*/
|
|
4504
|
+
addConstraint(constraint) {
|
|
4505
|
+
if (this._cnMap.find(constraint) !== void 0) throw new Error("duplicate constraint");
|
|
4506
|
+
let data = this._createRow(constraint);
|
|
4507
|
+
let row = data.row;
|
|
4508
|
+
let tag = data.tag;
|
|
4509
|
+
let subject = this._chooseSubject(row, tag);
|
|
4510
|
+
if (subject.type() === SymbolType.Invalid && row.allDummies()) if (!nearZero(row.constant())) throw new Error("unsatisfiable constraint");
|
|
4511
|
+
else subject = tag.marker;
|
|
4512
|
+
if (subject.type() === SymbolType.Invalid) {
|
|
4513
|
+
if (!this._addWithArtificialVariable(row)) throw new Error("unsatisfiable constraint");
|
|
4514
|
+
} else {
|
|
4515
|
+
row.solveFor(subject);
|
|
4516
|
+
this._substitute(subject, row);
|
|
4517
|
+
this._rowMap.insert(subject, row);
|
|
4518
|
+
}
|
|
4519
|
+
this._cnMap.insert(constraint, tag);
|
|
4520
|
+
this._optimize(this._objective);
|
|
4521
|
+
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Remove a constraint from the solver.
|
|
4524
|
+
*
|
|
4525
|
+
* @param {Constraint} constraint Constraint to remove from the solver
|
|
4526
|
+
*/
|
|
4527
|
+
removeConstraint(constraint) {
|
|
4528
|
+
let cnPair = this._cnMap.erase(constraint);
|
|
4529
|
+
if (cnPair === void 0) throw new Error("unknown constraint");
|
|
4530
|
+
this._removeConstraintEffects(constraint, cnPair.second);
|
|
4531
|
+
let marker = cnPair.second.marker;
|
|
4532
|
+
let rowPair = this._rowMap.erase(marker);
|
|
4533
|
+
if (rowPair === void 0) {
|
|
4534
|
+
let leaving = this._getMarkerLeavingSymbol(marker);
|
|
4535
|
+
if (leaving.type() === SymbolType.Invalid) throw new Error("failed to find leaving row");
|
|
4536
|
+
rowPair = this._rowMap.erase(leaving);
|
|
4537
|
+
rowPair.second.solveForEx(leaving, marker);
|
|
4538
|
+
this._substitute(marker, rowPair.second);
|
|
4539
|
+
}
|
|
4540
|
+
this._optimize(this._objective);
|
|
4541
|
+
}
|
|
4542
|
+
/**
|
|
4543
|
+
* Test whether the solver contains the constraint.
|
|
4544
|
+
*
|
|
4545
|
+
* @param {Constraint} constraint Constraint to test for
|
|
4546
|
+
* @return {Bool} true or false
|
|
4547
|
+
*/
|
|
4548
|
+
hasConstraint(constraint) {
|
|
4549
|
+
return this._cnMap.contains(constraint);
|
|
4550
|
+
}
|
|
4551
|
+
/**
|
|
4552
|
+
* Get an array of the current constraints.
|
|
4553
|
+
*
|
|
4554
|
+
* @return {Constraint[]}
|
|
4555
|
+
*/
|
|
4556
|
+
getConstraints() {
|
|
4557
|
+
return this._cnMap.array.map(({ first }) => first);
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* Add an edit variable to the solver.
|
|
4561
|
+
*
|
|
4562
|
+
* @param {Variable} variable Edit variable to add to the solver
|
|
4563
|
+
* @param {Number} strength Strength, should be less than `Strength.required`
|
|
4564
|
+
*/
|
|
4565
|
+
addEditVariable(variable, strength) {
|
|
4566
|
+
if (this._editMap.find(variable) !== void 0) throw new Error("duplicate edit variable");
|
|
4567
|
+
strength = Strength.clip(strength);
|
|
4568
|
+
if (strength === Strength.required) throw new Error("bad required strength");
|
|
4569
|
+
let cn = new Constraint(new Expression(variable), Operator.Eq, void 0, strength);
|
|
4570
|
+
this.addConstraint(cn);
|
|
4571
|
+
let info = {
|
|
4572
|
+
tag: this._cnMap.find(cn).second,
|
|
4573
|
+
constraint: cn,
|
|
4574
|
+
constant: 0
|
|
4575
|
+
};
|
|
4576
|
+
this._editMap.insert(variable, info);
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Remove an edit variable from the solver.
|
|
4580
|
+
*
|
|
4581
|
+
* @param {Variable} variable Edit variable to remove from the solver
|
|
4582
|
+
*/
|
|
4583
|
+
removeEditVariable(variable) {
|
|
4584
|
+
let editPair = this._editMap.erase(variable);
|
|
4585
|
+
if (editPair === void 0) throw new Error("unknown edit variable");
|
|
4586
|
+
this.removeConstraint(editPair.second.constraint);
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Test whether the solver contains the edit variable.
|
|
4590
|
+
*
|
|
4591
|
+
* @param {Variable} variable Edit variable to test for
|
|
4592
|
+
* @return {Bool} true or false
|
|
4593
|
+
*/
|
|
4594
|
+
hasEditVariable(variable) {
|
|
4595
|
+
return this._editMap.contains(variable);
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Suggest the value of an edit variable.
|
|
4599
|
+
*
|
|
4600
|
+
* @param {Variable} variable Edit variable to suggest a value for
|
|
4601
|
+
* @param {Number} value Suggested value
|
|
4602
|
+
*/
|
|
4603
|
+
suggestValue(variable, value) {
|
|
4604
|
+
let editPair = this._editMap.find(variable);
|
|
4605
|
+
if (editPair === void 0) throw new Error("unknown edit variable");
|
|
4606
|
+
let rows = this._rowMap;
|
|
4607
|
+
let info = editPair.second;
|
|
4608
|
+
let delta = value - info.constant;
|
|
4609
|
+
info.constant = value;
|
|
4610
|
+
let marker = info.tag.marker;
|
|
4611
|
+
let rowPair = rows.find(marker);
|
|
4612
|
+
if (rowPair !== void 0) {
|
|
4613
|
+
if (rowPair.second.add(-delta) < 0) this._infeasibleRows.push(marker);
|
|
4614
|
+
this._dualOptimize();
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
let other = info.tag.other;
|
|
4618
|
+
rowPair = rows.find(other);
|
|
4619
|
+
if (rowPair !== void 0) {
|
|
4620
|
+
if (rowPair.second.add(delta) < 0) this._infeasibleRows.push(other);
|
|
4621
|
+
this._dualOptimize();
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
for (let i = 0, n = rows.size(); i < n; ++i) {
|
|
4625
|
+
let rowPair$1 = rows.itemAt(i);
|
|
4626
|
+
let row = rowPair$1.second;
|
|
4627
|
+
let coeff = row.coefficientFor(marker);
|
|
4628
|
+
if (coeff !== 0 && row.add(delta * coeff) < 0 && rowPair$1.first.type() !== SymbolType.External) this._infeasibleRows.push(rowPair$1.first);
|
|
4629
|
+
}
|
|
4630
|
+
this._dualOptimize();
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Update the values of the variables.
|
|
4634
|
+
*/
|
|
4635
|
+
updateVariables() {
|
|
4636
|
+
let vars = this._varMap;
|
|
4637
|
+
let rows = this._rowMap;
|
|
4638
|
+
for (let i = 0, n = vars.size(); i < n; ++i) {
|
|
4639
|
+
let pair = vars.itemAt(i);
|
|
4640
|
+
let rowPair = rows.find(pair.second);
|
|
4641
|
+
if (rowPair !== void 0) pair.first.setValue(rowPair.second.constant());
|
|
4642
|
+
else pair.first.setValue(0);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Get the symbol for the given variable.
|
|
4647
|
+
*
|
|
4648
|
+
* If a symbol does not exist for the variable, one will be created.
|
|
4649
|
+
* @private
|
|
4650
|
+
*/
|
|
4651
|
+
_getVarSymbol(variable) {
|
|
4652
|
+
let factory = () => this._makeSymbol(SymbolType.External);
|
|
4653
|
+
return this._varMap.setDefault(variable, factory).second;
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* Create a new Row object for the given constraint.
|
|
4657
|
+
*
|
|
4658
|
+
* The terms in the constraint will be converted to cells in the row.
|
|
4659
|
+
* Any term in the constraint with a coefficient of zero is ignored.
|
|
4660
|
+
* This method uses the `_getVarSymbol` method to get the symbol for
|
|
4661
|
+
* the variables added to the row. If the symbol for a given cell
|
|
4662
|
+
* variable is basic, the cell variable will be substituted with the
|
|
4663
|
+
* basic row.
|
|
4664
|
+
*
|
|
4665
|
+
* The necessary slack and error variables will be added to the row.
|
|
4666
|
+
* If the constant for the row is negative, the sign for the row
|
|
4667
|
+
* will be inverted so the constant becomes positive.
|
|
4668
|
+
*
|
|
4669
|
+
* Returns the created Row and the tag for tracking the constraint.
|
|
4670
|
+
* @private
|
|
4671
|
+
*/
|
|
4672
|
+
_createRow(constraint) {
|
|
4673
|
+
let expr = constraint.expression();
|
|
4674
|
+
let row = new Row(expr.constant());
|
|
4675
|
+
let terms = expr.terms();
|
|
4676
|
+
for (let i = 0, n = terms.size(); i < n; ++i) {
|
|
4677
|
+
let termPair = terms.itemAt(i);
|
|
4678
|
+
if (!nearZero(termPair.second)) {
|
|
4679
|
+
let symbol = this._getVarSymbol(termPair.first);
|
|
4680
|
+
let basicPair = this._rowMap.find(symbol);
|
|
4681
|
+
if (basicPair !== void 0) row.insertRow(basicPair.second, termPair.second);
|
|
4682
|
+
else row.insertSymbol(symbol, termPair.second);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
let objective = this._objective;
|
|
4686
|
+
let strength = constraint.strength();
|
|
4687
|
+
let tag = {
|
|
4688
|
+
marker: INVALID_SYMBOL,
|
|
4689
|
+
other: INVALID_SYMBOL
|
|
4690
|
+
};
|
|
4691
|
+
switch (constraint.op()) {
|
|
4692
|
+
case Operator.Le:
|
|
4693
|
+
case Operator.Ge: {
|
|
4694
|
+
let coeff = constraint.op() === Operator.Le ? 1 : -1;
|
|
4695
|
+
let slack = this._makeSymbol(SymbolType.Slack);
|
|
4696
|
+
tag.marker = slack;
|
|
4697
|
+
row.insertSymbol(slack, coeff);
|
|
4698
|
+
if (strength < Strength.required) {
|
|
4699
|
+
let error = this._makeSymbol(SymbolType.Error);
|
|
4700
|
+
tag.other = error;
|
|
4701
|
+
row.insertSymbol(error, -coeff);
|
|
4702
|
+
objective.insertSymbol(error, strength);
|
|
4703
|
+
}
|
|
4704
|
+
break;
|
|
4705
|
+
}
|
|
4706
|
+
case Operator.Eq:
|
|
4707
|
+
if (strength < Strength.required) {
|
|
4708
|
+
let errplus = this._makeSymbol(SymbolType.Error);
|
|
4709
|
+
let errminus = this._makeSymbol(SymbolType.Error);
|
|
4710
|
+
tag.marker = errplus;
|
|
4711
|
+
tag.other = errminus;
|
|
4712
|
+
row.insertSymbol(errplus, -1);
|
|
4713
|
+
row.insertSymbol(errminus, 1);
|
|
4714
|
+
objective.insertSymbol(errplus, strength);
|
|
4715
|
+
objective.insertSymbol(errminus, strength);
|
|
4716
|
+
} else {
|
|
4717
|
+
let dummy = this._makeSymbol(SymbolType.Dummy);
|
|
4718
|
+
tag.marker = dummy;
|
|
4719
|
+
row.insertSymbol(dummy);
|
|
4720
|
+
}
|
|
4721
|
+
break;
|
|
4722
|
+
}
|
|
4723
|
+
if (row.constant() < 0) row.reverseSign();
|
|
4724
|
+
return {
|
|
4725
|
+
row,
|
|
4726
|
+
tag
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
/**
|
|
4730
|
+
* Choose the subject for solving for the row.
|
|
4731
|
+
*
|
|
4732
|
+
* This method will choose the best subject for using as the solve
|
|
4733
|
+
* target for the row. An invalid symbol will be returned if there
|
|
4734
|
+
* is no valid target.
|
|
4735
|
+
*
|
|
4736
|
+
* The symbols are chosen according to the following precedence:
|
|
4737
|
+
*
|
|
4738
|
+
* 1) The first symbol representing an external variable.
|
|
4739
|
+
* 2) A negative slack or error tag variable.
|
|
4740
|
+
*
|
|
4741
|
+
* If a subject cannot be found, an invalid symbol will be returned.
|
|
4742
|
+
*
|
|
4743
|
+
* @private
|
|
4744
|
+
*/
|
|
4745
|
+
_chooseSubject(row, tag) {
|
|
4746
|
+
let cells = row.cells();
|
|
4747
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
4748
|
+
let pair = cells.itemAt(i);
|
|
4749
|
+
if (pair.first.type() === SymbolType.External) return pair.first;
|
|
4750
|
+
}
|
|
4751
|
+
let type = tag.marker.type();
|
|
4752
|
+
if (type === SymbolType.Slack || type === SymbolType.Error) {
|
|
4753
|
+
if (row.coefficientFor(tag.marker) < 0) return tag.marker;
|
|
4754
|
+
}
|
|
4755
|
+
type = tag.other.type();
|
|
4756
|
+
if (type === SymbolType.Slack || type === SymbolType.Error) {
|
|
4757
|
+
if (row.coefficientFor(tag.other) < 0) return tag.other;
|
|
4758
|
+
}
|
|
4759
|
+
return INVALID_SYMBOL;
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Add the row to the tableau using an artificial variable.
|
|
4763
|
+
*
|
|
4764
|
+
* This will return false if the constraint cannot be satisfied.
|
|
4765
|
+
*
|
|
4766
|
+
* @private
|
|
4767
|
+
*/
|
|
4768
|
+
_addWithArtificialVariable(row) {
|
|
4769
|
+
let art = this._makeSymbol(SymbolType.Slack);
|
|
4770
|
+
this._rowMap.insert(art, row.copy());
|
|
4771
|
+
this._artificial = row.copy();
|
|
4772
|
+
this._optimize(this._artificial);
|
|
4773
|
+
let success = nearZero(this._artificial.constant());
|
|
4774
|
+
this._artificial = null;
|
|
4775
|
+
let pair = this._rowMap.erase(art);
|
|
4776
|
+
if (pair !== void 0) {
|
|
4777
|
+
let basicRow = pair.second;
|
|
4778
|
+
if (basicRow.isConstant()) return success;
|
|
4779
|
+
let entering = this._anyPivotableSymbol(basicRow);
|
|
4780
|
+
if (entering.type() === SymbolType.Invalid) return false;
|
|
4781
|
+
basicRow.solveForEx(art, entering);
|
|
4782
|
+
this._substitute(entering, basicRow);
|
|
4783
|
+
this._rowMap.insert(entering, basicRow);
|
|
4784
|
+
}
|
|
4785
|
+
let rows = this._rowMap;
|
|
4786
|
+
for (let i = 0, n = rows.size(); i < n; ++i) rows.itemAt(i).second.removeSymbol(art);
|
|
4787
|
+
this._objective.removeSymbol(art);
|
|
4788
|
+
return success;
|
|
4789
|
+
}
|
|
4790
|
+
/**
|
|
4791
|
+
* Substitute the parametric symbol with the given row.
|
|
4792
|
+
*
|
|
4793
|
+
* This method will substitute all instances of the parametric symbol
|
|
4794
|
+
* in the tableau and the objective function with the given row.
|
|
4795
|
+
*
|
|
4796
|
+
* @private
|
|
4797
|
+
*/
|
|
4798
|
+
_substitute(symbol, row) {
|
|
4799
|
+
let rows = this._rowMap;
|
|
4800
|
+
for (let i = 0, n = rows.size(); i < n; ++i) {
|
|
4801
|
+
let pair = rows.itemAt(i);
|
|
4802
|
+
pair.second.substitute(symbol, row);
|
|
4803
|
+
if (pair.second.constant() < 0 && pair.first.type() !== SymbolType.External) this._infeasibleRows.push(pair.first);
|
|
4804
|
+
}
|
|
4805
|
+
this._objective.substitute(symbol, row);
|
|
4806
|
+
if (this._artificial) this._artificial.substitute(symbol, row);
|
|
4807
|
+
}
|
|
4808
|
+
/**
|
|
4809
|
+
* Optimize the system for the given objective function.
|
|
4810
|
+
*
|
|
4811
|
+
* This method performs iterations of Phase 2 of the simplex method
|
|
4812
|
+
* until the objective function reaches a minimum.
|
|
4813
|
+
*
|
|
4814
|
+
* @private
|
|
4815
|
+
*/
|
|
4816
|
+
_optimize(objective) {
|
|
4817
|
+
let iterations = 0;
|
|
4818
|
+
while (iterations < this.maxIterations) {
|
|
4819
|
+
let entering = this._getEnteringSymbol(objective);
|
|
4820
|
+
if (entering.type() === SymbolType.Invalid) return;
|
|
4821
|
+
let leaving = this._getLeavingSymbol(entering);
|
|
4822
|
+
if (leaving.type() === SymbolType.Invalid) throw new Error("the objective is unbounded");
|
|
4823
|
+
let row = this._rowMap.erase(leaving).second;
|
|
4824
|
+
row.solveForEx(leaving, entering);
|
|
4825
|
+
this._substitute(entering, row);
|
|
4826
|
+
this._rowMap.insert(entering, row);
|
|
4827
|
+
iterations++;
|
|
4828
|
+
}
|
|
4829
|
+
throw new Error("solver iterations exceeded");
|
|
4830
|
+
}
|
|
4831
|
+
/**
|
|
4832
|
+
* Optimize the system using the dual of the simplex method.
|
|
4833
|
+
*
|
|
4834
|
+
* The current state of the system should be such that the objective
|
|
4835
|
+
* function is optimal, but not feasible. This method will perform
|
|
4836
|
+
* an iteration of the dual simplex method to make the solution both
|
|
4837
|
+
* optimal and feasible.
|
|
4838
|
+
*
|
|
4839
|
+
* @private
|
|
4840
|
+
*/
|
|
4841
|
+
_dualOptimize() {
|
|
4842
|
+
let rows = this._rowMap;
|
|
4843
|
+
let infeasible = this._infeasibleRows;
|
|
4844
|
+
while (infeasible.length !== 0) {
|
|
4845
|
+
let leaving = infeasible.pop();
|
|
4846
|
+
let pair = rows.find(leaving);
|
|
4847
|
+
if (pair !== void 0 && pair.second.constant() < 0) {
|
|
4848
|
+
let entering = this._getDualEnteringSymbol(pair.second);
|
|
4849
|
+
if (entering.type() === SymbolType.Invalid) throw new Error("dual optimize failed");
|
|
4850
|
+
let row = pair.second;
|
|
4851
|
+
rows.erase(leaving);
|
|
4852
|
+
row.solveForEx(leaving, entering);
|
|
4853
|
+
this._substitute(entering, row);
|
|
4854
|
+
rows.insert(entering, row);
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Compute the entering variable for a pivot operation.
|
|
4860
|
+
*
|
|
4861
|
+
* This method will return first symbol in the objective function which
|
|
4862
|
+
* is non-dummy and has a coefficient less than zero. If no symbol meets
|
|
4863
|
+
* the criteria, it means the objective function is at a minimum, and an
|
|
4864
|
+
* invalid symbol is returned.
|
|
4865
|
+
*
|
|
4866
|
+
* @private
|
|
4867
|
+
*/
|
|
4868
|
+
_getEnteringSymbol(objective) {
|
|
4869
|
+
let cells = objective.cells();
|
|
4870
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
4871
|
+
let pair = cells.itemAt(i);
|
|
4872
|
+
let symbol = pair.first;
|
|
4873
|
+
if (pair.second < 0 && symbol.type() !== SymbolType.Dummy) return symbol;
|
|
4874
|
+
}
|
|
4875
|
+
return INVALID_SYMBOL;
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* Compute the entering symbol for the dual optimize operation.
|
|
4879
|
+
*
|
|
4880
|
+
* This method will return the symbol in the row which has a positive
|
|
4881
|
+
* coefficient and yields the minimum ratio for its respective symbol
|
|
4882
|
+
* in the objective function. The provided row *must* be infeasible.
|
|
4883
|
+
* If no symbol is found which meats the criteria, an invalid symbol
|
|
4884
|
+
* is returned.
|
|
4885
|
+
*
|
|
4886
|
+
* @private
|
|
4887
|
+
*/
|
|
4888
|
+
_getDualEnteringSymbol(row) {
|
|
4889
|
+
let ratio = Number.MAX_VALUE;
|
|
4890
|
+
let entering = INVALID_SYMBOL;
|
|
4891
|
+
let cells = row.cells();
|
|
4892
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
4893
|
+
let pair = cells.itemAt(i);
|
|
4894
|
+
let symbol = pair.first;
|
|
4895
|
+
let c$1 = pair.second;
|
|
4896
|
+
if (c$1 > 0 && symbol.type() !== SymbolType.Dummy) {
|
|
4897
|
+
let r = this._objective.coefficientFor(symbol) / c$1;
|
|
4898
|
+
if (r < ratio) {
|
|
4899
|
+
ratio = r;
|
|
4900
|
+
entering = symbol;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
return entering;
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Compute the symbol for pivot exit row.
|
|
4908
|
+
*
|
|
4909
|
+
* This method will return the symbol for the exit row in the row
|
|
4910
|
+
* map. If no appropriate exit symbol is found, an invalid symbol
|
|
4911
|
+
* will be returned. This indicates that the objective function is
|
|
4912
|
+
* unbounded.
|
|
4913
|
+
*
|
|
4914
|
+
* @private
|
|
4915
|
+
*/
|
|
4916
|
+
_getLeavingSymbol(entering) {
|
|
4917
|
+
let ratio = Number.MAX_VALUE;
|
|
4918
|
+
let found = INVALID_SYMBOL;
|
|
4919
|
+
let rows = this._rowMap;
|
|
4920
|
+
for (let i = 0, n = rows.size(); i < n; ++i) {
|
|
4921
|
+
let pair = rows.itemAt(i);
|
|
4922
|
+
let symbol = pair.first;
|
|
4923
|
+
if (symbol.type() !== SymbolType.External) {
|
|
4924
|
+
let row = pair.second;
|
|
4925
|
+
let temp = row.coefficientFor(entering);
|
|
4926
|
+
if (temp < 0) {
|
|
4927
|
+
let temp_ratio = -row.constant() / temp;
|
|
4928
|
+
if (temp_ratio < ratio) {
|
|
4929
|
+
ratio = temp_ratio;
|
|
4930
|
+
found = symbol;
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return found;
|
|
4936
|
+
}
|
|
4937
|
+
/**
|
|
4938
|
+
* Compute the leaving symbol for a marker variable.
|
|
4939
|
+
*
|
|
4940
|
+
* This method will return a symbol corresponding to a basic row
|
|
4941
|
+
* which holds the given marker variable. The row will be chosen
|
|
4942
|
+
* according to the following precedence:
|
|
4943
|
+
*
|
|
4944
|
+
* 1) The row with a restricted basic varible and a negative coefficient
|
|
4945
|
+
* for the marker with the smallest ratio of -constant / coefficient.
|
|
4946
|
+
*
|
|
4947
|
+
* 2) The row with a restricted basic variable and the smallest ratio
|
|
4948
|
+
* of constant / coefficient.
|
|
4949
|
+
*
|
|
4950
|
+
* 3) The last unrestricted row which contains the marker.
|
|
4951
|
+
*
|
|
4952
|
+
* If the marker does not exist in any row, an invalid symbol will be
|
|
4953
|
+
* returned. This indicates an internal solver error since the marker
|
|
4954
|
+
* *should* exist somewhere in the tableau.
|
|
4955
|
+
*
|
|
4956
|
+
* @private
|
|
4957
|
+
*/
|
|
4958
|
+
_getMarkerLeavingSymbol(marker) {
|
|
4959
|
+
let dmax = Number.MAX_VALUE;
|
|
4960
|
+
let r1 = dmax;
|
|
4961
|
+
let r2 = dmax;
|
|
4962
|
+
let invalid = INVALID_SYMBOL;
|
|
4963
|
+
let first = invalid;
|
|
4964
|
+
let second = invalid;
|
|
4965
|
+
let third = invalid;
|
|
4966
|
+
let rows = this._rowMap;
|
|
4967
|
+
for (let i = 0, n = rows.size(); i < n; ++i) {
|
|
4968
|
+
let pair = rows.itemAt(i);
|
|
4969
|
+
let row = pair.second;
|
|
4970
|
+
let c$1 = row.coefficientFor(marker);
|
|
4971
|
+
if (c$1 === 0) continue;
|
|
4972
|
+
let symbol = pair.first;
|
|
4973
|
+
if (symbol.type() === SymbolType.External) third = symbol;
|
|
4974
|
+
else if (c$1 < 0) {
|
|
4975
|
+
let r = -row.constant() / c$1;
|
|
4976
|
+
if (r < r1) {
|
|
4977
|
+
r1 = r;
|
|
4978
|
+
first = symbol;
|
|
4979
|
+
}
|
|
4980
|
+
} else {
|
|
4981
|
+
let r = row.constant() / c$1;
|
|
4982
|
+
if (r < r2) {
|
|
4983
|
+
r2 = r;
|
|
4984
|
+
second = symbol;
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
if (first !== invalid) return first;
|
|
4989
|
+
if (second !== invalid) return second;
|
|
4990
|
+
return third;
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Remove the effects of a constraint on the objective function.
|
|
4994
|
+
*
|
|
4995
|
+
* @private
|
|
4996
|
+
*/
|
|
4997
|
+
_removeConstraintEffects(cn, tag) {
|
|
4998
|
+
if (tag.marker.type() === SymbolType.Error) this._removeMarkerEffects(tag.marker, cn.strength());
|
|
4999
|
+
if (tag.other.type() === SymbolType.Error) this._removeMarkerEffects(tag.other, cn.strength());
|
|
5000
|
+
}
|
|
5001
|
+
/**
|
|
5002
|
+
* Remove the effects of an error marker on the objective function.
|
|
5003
|
+
*
|
|
5004
|
+
* @private
|
|
5005
|
+
*/
|
|
5006
|
+
_removeMarkerEffects(marker, strength) {
|
|
5007
|
+
let pair = this._rowMap.find(marker);
|
|
5008
|
+
if (pair !== void 0) this._objective.insertRow(pair.second, -strength);
|
|
5009
|
+
else this._objective.insertSymbol(marker, -strength);
|
|
5010
|
+
}
|
|
5011
|
+
/**
|
|
5012
|
+
* Get the first Slack or Error symbol in the row.
|
|
5013
|
+
*
|
|
5014
|
+
* If no such symbol is present, an invalid symbol will be returned.
|
|
5015
|
+
*
|
|
5016
|
+
* @private
|
|
5017
|
+
*/
|
|
5018
|
+
_anyPivotableSymbol(row) {
|
|
5019
|
+
let cells = row.cells();
|
|
5020
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
5021
|
+
let pair = cells.itemAt(i);
|
|
5022
|
+
let type = pair.first.type();
|
|
5023
|
+
if (type === SymbolType.Slack || type === SymbolType.Error) return pair.first;
|
|
5024
|
+
}
|
|
5025
|
+
return INVALID_SYMBOL;
|
|
5026
|
+
}
|
|
5027
|
+
/**
|
|
5028
|
+
* Returns a new Symbol of the given type.
|
|
5029
|
+
*
|
|
5030
|
+
* @private
|
|
5031
|
+
*/
|
|
5032
|
+
_makeSymbol(type) {
|
|
5033
|
+
return new Symbol(type, this._idTick++);
|
|
5034
|
+
}
|
|
5035
|
+
_cnMap = createCnMap();
|
|
5036
|
+
_rowMap = createRowMap();
|
|
5037
|
+
_varMap = createVarMap();
|
|
5038
|
+
_editMap = createEditMap();
|
|
5039
|
+
_infeasibleRows = [];
|
|
5040
|
+
_objective = new Row();
|
|
5041
|
+
_artificial = null;
|
|
5042
|
+
_idTick = 0;
|
|
5043
|
+
};
|
|
5044
|
+
/**
|
|
5045
|
+
* Test whether a value is approximately zero.
|
|
5046
|
+
* @private
|
|
5047
|
+
*/
|
|
5048
|
+
function nearZero(value) {
|
|
5049
|
+
let eps = 1e-8;
|
|
5050
|
+
return value < 0 ? -value < eps : value < eps;
|
|
5051
|
+
}
|
|
5052
|
+
/**
|
|
5053
|
+
* An internal function for creating a constraint map.
|
|
5054
|
+
* @private
|
|
5055
|
+
*/
|
|
5056
|
+
function createCnMap() {
|
|
5057
|
+
return createMap();
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* An internal function for creating a row map.
|
|
5061
|
+
* @private
|
|
5062
|
+
*/
|
|
5063
|
+
function createRowMap() {
|
|
5064
|
+
return createMap();
|
|
5065
|
+
}
|
|
5066
|
+
/**
|
|
5067
|
+
* An internal function for creating a variable map.
|
|
5068
|
+
* @private
|
|
5069
|
+
*/
|
|
5070
|
+
function createVarMap() {
|
|
5071
|
+
return createMap();
|
|
5072
|
+
}
|
|
5073
|
+
/**
|
|
5074
|
+
* An internal function for creating an edit map.
|
|
5075
|
+
* @private
|
|
5076
|
+
*/
|
|
5077
|
+
function createEditMap() {
|
|
5078
|
+
return createMap();
|
|
5079
|
+
}
|
|
5080
|
+
/**
|
|
5081
|
+
* An enum defining the available symbol types.
|
|
5082
|
+
* @private
|
|
5083
|
+
*/
|
|
5084
|
+
var SymbolType;
|
|
5085
|
+
(function(SymbolType$1) {
|
|
5086
|
+
SymbolType$1[SymbolType$1["Invalid"] = 0] = "Invalid";
|
|
5087
|
+
SymbolType$1[SymbolType$1["External"] = 1] = "External";
|
|
5088
|
+
SymbolType$1[SymbolType$1["Slack"] = 2] = "Slack";
|
|
5089
|
+
SymbolType$1[SymbolType$1["Error"] = 3] = "Error";
|
|
5090
|
+
SymbolType$1[SymbolType$1["Dummy"] = 4] = "Dummy";
|
|
5091
|
+
})(SymbolType || (SymbolType = {}));
|
|
5092
|
+
/**
|
|
5093
|
+
* An internal class representing a symbol in the solver.
|
|
5094
|
+
* @private
|
|
5095
|
+
*/
|
|
5096
|
+
var Symbol = class {
|
|
5097
|
+
/**
|
|
5098
|
+
* Construct a new Symbol
|
|
5099
|
+
*
|
|
5100
|
+
* @param [type] The type of the symbol.
|
|
5101
|
+
* @param [id] The unique id number of the symbol.
|
|
5102
|
+
*/
|
|
5103
|
+
constructor(type, id) {
|
|
5104
|
+
this._id = id;
|
|
5105
|
+
this._type = type;
|
|
5106
|
+
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Returns the unique id number of the symbol.
|
|
5109
|
+
*/
|
|
5110
|
+
id() {
|
|
5111
|
+
return this._id;
|
|
5112
|
+
}
|
|
5113
|
+
/**
|
|
5114
|
+
* Returns the type of the symbol.
|
|
5115
|
+
*/
|
|
5116
|
+
type() {
|
|
5117
|
+
return this._type;
|
|
5118
|
+
}
|
|
5119
|
+
_id;
|
|
5120
|
+
_type;
|
|
5121
|
+
};
|
|
5122
|
+
/**
|
|
5123
|
+
* A static invalid symbol
|
|
5124
|
+
* @private
|
|
5125
|
+
*/
|
|
5126
|
+
let INVALID_SYMBOL = new Symbol(SymbolType.Invalid, -1);
|
|
5127
|
+
/**
|
|
5128
|
+
* An internal row class used by the solver.
|
|
5129
|
+
* @private
|
|
5130
|
+
*/
|
|
5131
|
+
var Row = class Row {
|
|
5132
|
+
/**
|
|
5133
|
+
* Construct a new Row.
|
|
5134
|
+
*/
|
|
5135
|
+
constructor(constant = 0) {
|
|
5136
|
+
this._constant = constant;
|
|
5137
|
+
}
|
|
5138
|
+
/**
|
|
5139
|
+
* Returns the mapping of symbols to coefficients.
|
|
5140
|
+
*/
|
|
5141
|
+
cells() {
|
|
5142
|
+
return this._cellMap;
|
|
5143
|
+
}
|
|
5144
|
+
/**
|
|
5145
|
+
* Returns the constant for the row.
|
|
5146
|
+
*/
|
|
5147
|
+
constant() {
|
|
5148
|
+
return this._constant;
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Returns true if the row is a constant value.
|
|
5152
|
+
*/
|
|
5153
|
+
isConstant() {
|
|
5154
|
+
return this._cellMap.empty();
|
|
5155
|
+
}
|
|
5156
|
+
/**
|
|
5157
|
+
* Returns true if the Row has all dummy symbols.
|
|
5158
|
+
*/
|
|
5159
|
+
allDummies() {
|
|
5160
|
+
let cells = this._cellMap;
|
|
5161
|
+
for (let i = 0, n = cells.size(); i < n; ++i) if (cells.itemAt(i).first.type() !== SymbolType.Dummy) return false;
|
|
5162
|
+
return true;
|
|
5163
|
+
}
|
|
5164
|
+
/**
|
|
5165
|
+
* Create a copy of the row.
|
|
5166
|
+
*/
|
|
5167
|
+
copy() {
|
|
5168
|
+
let theCopy = new Row(this._constant);
|
|
5169
|
+
theCopy._cellMap = this._cellMap.copy();
|
|
5170
|
+
return theCopy;
|
|
5171
|
+
}
|
|
5172
|
+
/**
|
|
5173
|
+
* Add a constant value to the row constant.
|
|
5174
|
+
*
|
|
5175
|
+
* Returns the new value of the constant.
|
|
5176
|
+
*/
|
|
5177
|
+
add(value) {
|
|
5178
|
+
return this._constant += value;
|
|
5179
|
+
}
|
|
5180
|
+
/**
|
|
5181
|
+
* Insert the symbol into the row with the given coefficient.
|
|
5182
|
+
*
|
|
5183
|
+
* If the symbol already exists in the row, the coefficient
|
|
5184
|
+
* will be added to the existing coefficient. If the resulting
|
|
5185
|
+
* coefficient is zero, the symbol will be removed from the row.
|
|
5186
|
+
*/
|
|
5187
|
+
insertSymbol(symbol, coefficient = 1) {
|
|
5188
|
+
let pair = this._cellMap.setDefault(symbol, () => 0);
|
|
5189
|
+
if (nearZero(pair.second += coefficient)) this._cellMap.erase(symbol);
|
|
5190
|
+
}
|
|
5191
|
+
/**
|
|
5192
|
+
* Insert a row into this row with a given coefficient.
|
|
5193
|
+
*
|
|
5194
|
+
* The constant and the cells of the other row will be
|
|
5195
|
+
* multiplied by the coefficient and added to this row. Any
|
|
5196
|
+
* cell with a resulting coefficient of zero will be removed
|
|
5197
|
+
* from the row.
|
|
5198
|
+
*/
|
|
5199
|
+
insertRow(other, coefficient = 1) {
|
|
5200
|
+
this._constant += other._constant * coefficient;
|
|
5201
|
+
let cells = other._cellMap;
|
|
5202
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
5203
|
+
let pair = cells.itemAt(i);
|
|
5204
|
+
this.insertSymbol(pair.first, pair.second * coefficient);
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Remove a symbol from the row.
|
|
5209
|
+
*/
|
|
5210
|
+
removeSymbol(symbol) {
|
|
5211
|
+
this._cellMap.erase(symbol);
|
|
5212
|
+
}
|
|
5213
|
+
/**
|
|
5214
|
+
* Reverse the sign of the constant and cells in the row.
|
|
5215
|
+
*/
|
|
5216
|
+
reverseSign() {
|
|
5217
|
+
this._constant = -this._constant;
|
|
5218
|
+
let cells = this._cellMap;
|
|
5219
|
+
for (let i = 0, n = cells.size(); i < n; ++i) {
|
|
5220
|
+
let pair = cells.itemAt(i);
|
|
5221
|
+
pair.second = -pair.second;
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
/**
|
|
5225
|
+
* Solve the row for the given symbol.
|
|
5226
|
+
*
|
|
5227
|
+
* This method assumes the row is of the form
|
|
5228
|
+
* a * x + b * y + c = 0 and (assuming solve for x) will modify
|
|
5229
|
+
* the row to represent the right hand side of
|
|
5230
|
+
* x = -b/a * y - c / a. The target symbol will be removed from
|
|
5231
|
+
* the row, and the constant and other cells will be multiplied
|
|
5232
|
+
* by the negative inverse of the target coefficient.
|
|
5233
|
+
*
|
|
5234
|
+
* The given symbol *must* exist in the row.
|
|
5235
|
+
*/
|
|
5236
|
+
solveFor(symbol) {
|
|
5237
|
+
let cells = this._cellMap;
|
|
5238
|
+
let coeff = -1 / cells.erase(symbol).second;
|
|
5239
|
+
this._constant *= coeff;
|
|
5240
|
+
for (let i = 0, n = cells.size(); i < n; ++i) cells.itemAt(i).second *= coeff;
|
|
5241
|
+
}
|
|
5242
|
+
/**
|
|
5243
|
+
* Solve the row for the given symbols.
|
|
5244
|
+
*
|
|
5245
|
+
* This method assumes the row is of the form
|
|
5246
|
+
* x = b * y + c and will solve the row such that
|
|
5247
|
+
* y = x / b - c / b. The rhs symbol will be removed from the
|
|
5248
|
+
* row, the lhs added, and the result divided by the negative
|
|
5249
|
+
* inverse of the rhs coefficient.
|
|
5250
|
+
*
|
|
5251
|
+
* The lhs symbol *must not* exist in the row, and the rhs
|
|
5252
|
+
* symbol must* exist in the row.
|
|
5253
|
+
*/
|
|
5254
|
+
solveForEx(lhs, rhs) {
|
|
5255
|
+
this.insertSymbol(lhs, -1);
|
|
5256
|
+
this.solveFor(rhs);
|
|
5257
|
+
}
|
|
5258
|
+
/**
|
|
5259
|
+
* Returns the coefficient for the given symbol.
|
|
5260
|
+
*/
|
|
5261
|
+
coefficientFor(symbol) {
|
|
5262
|
+
let pair = this._cellMap.find(symbol);
|
|
5263
|
+
return pair !== void 0 ? pair.second : 0;
|
|
5264
|
+
}
|
|
5265
|
+
/**
|
|
5266
|
+
* Substitute a symbol with the data from another row.
|
|
5267
|
+
*
|
|
5268
|
+
* Given a row of the form a * x + b and a substitution of the
|
|
5269
|
+
* form x = 3 * y + c the row will be updated to reflect the
|
|
5270
|
+
* expression 3 * a * y + a * c + b.
|
|
5271
|
+
*
|
|
5272
|
+
* If the symbol does not exist in the row, this is a no-op.
|
|
5273
|
+
*/
|
|
5274
|
+
substitute(symbol, row) {
|
|
5275
|
+
let pair = this._cellMap.erase(symbol);
|
|
5276
|
+
if (pair !== void 0) this.insertRow(row, pair.second);
|
|
5277
|
+
}
|
|
5278
|
+
_cellMap = createMap();
|
|
5279
|
+
_constant;
|
|
5280
|
+
};
|
|
5281
|
+
|
|
5282
|
+
//#endregion
|
|
5283
|
+
//#region src/constraints.ts
|
|
5284
|
+
/** Serializable constraint constructors; graph geometry stays in @statelyai/graph. */
|
|
5285
|
+
const c = {
|
|
5286
|
+
align: (value) => ({
|
|
5287
|
+
...value,
|
|
5288
|
+
kind: "align"
|
|
5289
|
+
}),
|
|
5290
|
+
distribute: (value) => ({
|
|
5291
|
+
...value,
|
|
5292
|
+
kind: "distribute"
|
|
5293
|
+
}),
|
|
5294
|
+
pin: (value) => ({
|
|
5295
|
+
...value,
|
|
5296
|
+
kind: "pin"
|
|
5297
|
+
}),
|
|
5298
|
+
linear: (value) => ({
|
|
5299
|
+
...value,
|
|
5300
|
+
kind: "linear"
|
|
5301
|
+
}),
|
|
5302
|
+
waypoint: (value) => ({
|
|
5303
|
+
...value,
|
|
5304
|
+
kind: "waypoint"
|
|
5305
|
+
})
|
|
5306
|
+
};
|
|
5307
|
+
function constraintFailure(constraint, message) {
|
|
5308
|
+
throw new LayoutError(`${constraint.id}: ${message}`, "UNSATISFIED_CONSTRAINT");
|
|
5309
|
+
}
|
|
5310
|
+
/** Solve positions only. Fixed geometry is represented by constants, never edit variables. */
|
|
5311
|
+
function solveGeometryConstraints(graph, constraints, movableNodes, movableLabels, diagnostics) {
|
|
5312
|
+
const solver = new Solver();
|
|
5313
|
+
const variables = /* @__PURE__ */ new Map();
|
|
5314
|
+
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5315
|
+
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
5316
|
+
function geometry(ref) {
|
|
5317
|
+
const node = "nodeId" in ref;
|
|
5318
|
+
const id = node ? ref.nodeId : ref.edgeId;
|
|
5319
|
+
const rect = node ? nodes.get(id) : edges.get(id);
|
|
5320
|
+
if (!rect) throw new LayoutError(`Unknown constraint entity: ${id}`, "INVALID_CONSTRAINT");
|
|
5321
|
+
const key = JSON.stringify([node ? "node" : "label", id]);
|
|
5322
|
+
if ((node ? movableNodes : movableLabels).has(id) && !variables.has(key)) {
|
|
5323
|
+
const pair = {
|
|
5324
|
+
x: new Variable(`${key}.x`),
|
|
5325
|
+
y: new Variable(`${key}.y`)
|
|
5326
|
+
};
|
|
5327
|
+
variables.set(key, pair);
|
|
5328
|
+
solver.addConstraint(new Constraint(pair.x, Operator.Eq, rect.x, Strength.weak / 10));
|
|
5329
|
+
solver.addConstraint(new Constraint(pair.y, Operator.Eq, rect.y, Strength.weak / 10));
|
|
5330
|
+
}
|
|
5331
|
+
return {
|
|
5332
|
+
rect,
|
|
5333
|
+
variables: variables.get(key)
|
|
5334
|
+
};
|
|
5335
|
+
}
|
|
5336
|
+
function expression(ref, attribute) {
|
|
5337
|
+
const { rect, variables: pair } = geometry(ref);
|
|
5338
|
+
const axis = attribute === "y" || attribute === "centerY" || attribute === "bottom" ? "y" : "x";
|
|
5339
|
+
const extent$1 = axis === "x" ? rect.width : rect.height;
|
|
5340
|
+
const offset = attribute.startsWith("center") ? extent$1 / 2 : attribute === "right" || attribute === "bottom" ? extent$1 : 0;
|
|
5341
|
+
return new Expression(pair?.[axis] ?? rect[axis], offset);
|
|
5342
|
+
}
|
|
5343
|
+
const checks = [];
|
|
5344
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5345
|
+
for (const constraint of constraints) {
|
|
5346
|
+
if (ids.has(constraint.id)) throw new LayoutError(`Duplicate constraint ID: ${constraint.id}`, "INVALID_CONSTRAINT");
|
|
5347
|
+
ids.add(constraint.id);
|
|
5348
|
+
if (constraint.kind === "waypoint") continue;
|
|
5349
|
+
const strength = Strength[constraint.strength ?? "required"];
|
|
5350
|
+
const add = (lhs, operator = Operator.Eq, rhs = 0) => {
|
|
5351
|
+
const difference = lhs.minus(rhs);
|
|
5352
|
+
if (!Number.isFinite(difference.constant()) || !difference.terms().array.every((item) => Number.isFinite(item.second))) throw new LayoutError(`${constraint.id}: non-finite expression`, "INVALID_CONSTRAINT");
|
|
5353
|
+
try {
|
|
5354
|
+
solver.addConstraint(new Constraint(difference, operator, 0, strength));
|
|
5355
|
+
} catch {
|
|
5356
|
+
constraintFailure(constraint, "conflicts with fixed geometry or another required constraint");
|
|
5357
|
+
}
|
|
5358
|
+
checks.push({
|
|
5359
|
+
constraint,
|
|
5360
|
+
expression: difference,
|
|
5361
|
+
operator
|
|
5362
|
+
});
|
|
5363
|
+
};
|
|
5364
|
+
if (constraint.kind === "linear") {
|
|
5365
|
+
let value = new Expression();
|
|
5366
|
+
for (const term of constraint.terms) value = value.plus(expression(term.entity, term.attribute).multiply(term.coefficient));
|
|
5367
|
+
add(value, {
|
|
5368
|
+
eq: Operator.Eq,
|
|
5369
|
+
le: Operator.Le,
|
|
5370
|
+
ge: Operator.Ge
|
|
5371
|
+
}[constraint.relation], constraint.value);
|
|
5372
|
+
} else if (constraint.kind === "pin") {
|
|
5373
|
+
geometry(constraint.entity);
|
|
5374
|
+
if (constraint.x !== void 0) add(expression(constraint.entity, "x"), Operator.Eq, constraint.x);
|
|
5375
|
+
if (constraint.y !== void 0) add(expression(constraint.entity, "y"), Operator.Eq, constraint.y);
|
|
5376
|
+
} else {
|
|
5377
|
+
const refs = constraint.entities;
|
|
5378
|
+
for (const ref of refs) geometry(ref);
|
|
5379
|
+
const axis = constraint.axis;
|
|
5380
|
+
const end = axis === "x" ? "right" : "bottom";
|
|
5381
|
+
const center = axis === "x" ? "centerX" : "centerY";
|
|
5382
|
+
if (constraint.kind === "align") {
|
|
5383
|
+
const attr = constraint.anchor === "start" ? axis : constraint.anchor === "end" ? end : center;
|
|
5384
|
+
for (const ref of refs.slice(1)) add(expression(ref, attr), Operator.Eq, expression(refs[0], attr));
|
|
5385
|
+
} else {
|
|
5386
|
+
const gaps = refs.slice(1).map((ref, i) => constraint.spacing === "centers" ? expression(ref, center).minus(expression(refs[i], center)) : expression(ref, axis).minus(expression(refs[i], end)));
|
|
5387
|
+
for (const gap of gaps) {
|
|
5388
|
+
add(gap, Operator.Ge, 0);
|
|
5389
|
+
add(gap, Operator.Eq, constraint.gap ?? gaps[0]);
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
solver.updateVariables();
|
|
5395
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5396
|
+
for (const check of checks) {
|
|
5397
|
+
const residual = check.expression.value();
|
|
5398
|
+
if ((check.operator === Operator.Eq ? Math.abs(residual) > 1e-6 : check.operator === Operator.Le ? residual > 1e-6 : residual < -1e-6) && !warned.has(check.constraint.id)) {
|
|
5399
|
+
warned.add(check.constraint.id);
|
|
5400
|
+
diagnostics.push({
|
|
5401
|
+
severity: "warning",
|
|
5402
|
+
code: "CONSTRAINT_VIOLATION",
|
|
5403
|
+
message: `Constraint ${check.constraint.id} could not be satisfied`,
|
|
5404
|
+
constraintIds: [check.constraint.id]
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
const coordinate = (variable) => {
|
|
5409
|
+
const value = variable.value();
|
|
5410
|
+
if (!Number.isFinite(value)) throw new LayoutError("Constraint solution contains non-finite geometry", "INVALID_CONSTRAINT");
|
|
5411
|
+
return value === 0 ? 0 : value;
|
|
5412
|
+
};
|
|
5413
|
+
return {
|
|
5414
|
+
...graph,
|
|
5415
|
+
nodes: graph.nodes.map((node) => {
|
|
5416
|
+
const pair = variables.get(JSON.stringify(["node", node.id]));
|
|
5417
|
+
return pair ? {
|
|
5418
|
+
...node,
|
|
5419
|
+
x: coordinate(pair.x),
|
|
5420
|
+
y: coordinate(pair.y)
|
|
5421
|
+
} : node;
|
|
5422
|
+
}),
|
|
5423
|
+
edges: graph.edges.map((edge) => {
|
|
5424
|
+
const pair = variables.get(JSON.stringify(["label", edge.id]));
|
|
5425
|
+
return pair ? {
|
|
5426
|
+
...edge,
|
|
5427
|
+
x: coordinate(pair.x),
|
|
5428
|
+
y: coordinate(pair.y)
|
|
5429
|
+
} : edge;
|
|
5430
|
+
})
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
//#endregion
|
|
5435
|
+
//#region src/authoring/routing.ts
|
|
5436
|
+
function overlaps(a, b, gap = 0) {
|
|
5437
|
+
return a.width > 0 && a.height > 0 && b.width > 0 && b.height > 0 && a.x < b.x + b.width + gap && a.x + a.width + gap > b.x && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
|
|
5438
|
+
}
|
|
5439
|
+
/** Open-interior intersection: travelling along a rectangle boundary is allowed. */
|
|
5440
|
+
function crossesRect(a, b, rect) {
|
|
5441
|
+
let low = 0, high = 1;
|
|
5442
|
+
for (const axis of ["x", "y"]) {
|
|
5443
|
+
const delta = b[axis] - a[axis];
|
|
5444
|
+
const min = rect[axis], max = min + (axis === "x" ? rect.width : rect.height);
|
|
5445
|
+
if (Math.abs(delta) < 1e-10) {
|
|
5446
|
+
if (a[axis] <= min || a[axis] >= max) return false;
|
|
5447
|
+
} else {
|
|
5448
|
+
const t1 = (min - a[axis]) / delta, t2 = (max - a[axis]) / delta;
|
|
5449
|
+
low = Math.max(low, Math.min(t1, t2));
|
|
5450
|
+
high = Math.min(high, Math.max(t1, t2));
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
return low < high - 1e-10;
|
|
5454
|
+
}
|
|
5455
|
+
function routeCrosses(points, rect) {
|
|
5456
|
+
return points?.some((point, i) => i > 0 && crossesRect(points[i - 1], point, rect)) ?? false;
|
|
5457
|
+
}
|
|
5458
|
+
function endpoint(node, portName, direction, source) {
|
|
5459
|
+
if (portName !== void 0) {
|
|
5460
|
+
const port = node.ports?.find((p) => p.name === portName);
|
|
5461
|
+
if (port?.x !== void 0 && port.y !== void 0) return {
|
|
5462
|
+
x: node.x + port.x + (port.width ?? 0) / 2,
|
|
5463
|
+
y: node.y + port.y + (port.height ?? 0) / 2
|
|
5464
|
+
};
|
|
5465
|
+
throw new LayoutError(`Port ${portName} on ${node.id} requires positioned geometry`, "MISSING_GEOMETRY");
|
|
5466
|
+
}
|
|
5467
|
+
const horizontal = direction === "left" || direction === "right";
|
|
5468
|
+
const far = source === (direction === "right" || direction === "down");
|
|
5469
|
+
return horizontal ? {
|
|
5470
|
+
x: node.x + (far ? node.width : 0),
|
|
5471
|
+
y: node.y + node.height / 2
|
|
5472
|
+
} : {
|
|
5473
|
+
x: node.x + node.width / 2,
|
|
5474
|
+
y: node.y + (far ? node.height : 0)
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
/** Deterministic orthogonal visibility-grid search around fixed rectangles. */
|
|
5478
|
+
function connect(start, end, obstacles, check) {
|
|
5479
|
+
if (start.x === end.x && start.y === end.y) return [start];
|
|
5480
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
5481
|
+
check();
|
|
5482
|
+
const margin = 64 * 2 ** attempt;
|
|
5483
|
+
const left = Math.min(start.x, end.x) - margin;
|
|
5484
|
+
const right = Math.max(start.x, end.x) + margin;
|
|
5485
|
+
const top = Math.min(start.y, end.y) - margin;
|
|
5486
|
+
const bottom = Math.max(start.y, end.y) + margin;
|
|
5487
|
+
const local = obstacles.filter((r) => r.x <= right && r.x + r.width >= left && r.y <= bottom && r.y + r.height >= top);
|
|
5488
|
+
const result = search(start, end, local, obstacles, check);
|
|
5489
|
+
if (result || local.length === obstacles.length) return result;
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
function search(start, end, gridObstacles, obstacles, check) {
|
|
5493
|
+
const xs = [...new Set([
|
|
5494
|
+
start.x,
|
|
5495
|
+
end.x,
|
|
5496
|
+
...gridObstacles.flatMap((r) => [r.x, r.x + r.width])
|
|
5497
|
+
])].sort((a, b) => a - b);
|
|
5498
|
+
const ys = [...new Set([
|
|
5499
|
+
start.y,
|
|
5500
|
+
end.y,
|
|
5501
|
+
...gridObstacles.flatMap((r) => [r.y, r.y + r.height])
|
|
5502
|
+
])].sort((a, b) => a - b);
|
|
5503
|
+
if (xs.length * ys.length > 4e4) return void 0;
|
|
5504
|
+
const width = xs.length;
|
|
5505
|
+
const point = (id) => ({
|
|
5506
|
+
x: xs[id % width],
|
|
5507
|
+
y: ys[Math.floor(id / width)]
|
|
5508
|
+
});
|
|
5509
|
+
const from = ys.indexOf(start.y) * width + xs.indexOf(start.x);
|
|
5510
|
+
const to = ys.indexOf(end.y) * width + xs.indexOf(end.x);
|
|
5511
|
+
const distances = new Map([[from, 0]]);
|
|
5512
|
+
const previous = /* @__PURE__ */ new Map();
|
|
5513
|
+
const queue = [];
|
|
5514
|
+
function push(item) {
|
|
5515
|
+
queue.push(item);
|
|
5516
|
+
let i = queue.length - 1;
|
|
5517
|
+
while (i > 0) {
|
|
5518
|
+
const parent = i - 1 >> 1;
|
|
5519
|
+
if (queue[parent].cost <= item.cost) break;
|
|
5520
|
+
queue[i] = queue[parent];
|
|
5521
|
+
i = parent;
|
|
5522
|
+
}
|
|
5523
|
+
queue[i] = item;
|
|
5524
|
+
}
|
|
5525
|
+
function pop() {
|
|
5526
|
+
const first = queue[0];
|
|
5527
|
+
const last = queue.pop();
|
|
5528
|
+
if (queue.length) {
|
|
5529
|
+
let i = 0;
|
|
5530
|
+
while (i * 2 + 1 < queue.length) {
|
|
5531
|
+
let child = i * 2 + 1;
|
|
5532
|
+
if (child + 1 < queue.length && queue[child + 1].cost < queue[child].cost) child++;
|
|
5533
|
+
if (last.cost <= queue[child].cost) break;
|
|
5534
|
+
queue[i] = queue[child];
|
|
5535
|
+
i = child;
|
|
5536
|
+
}
|
|
5537
|
+
queue[i] = last;
|
|
5538
|
+
}
|
|
5539
|
+
return first;
|
|
5540
|
+
}
|
|
5541
|
+
push({
|
|
5542
|
+
id: from,
|
|
5543
|
+
cost: 0
|
|
5544
|
+
});
|
|
5545
|
+
let visits = 0;
|
|
5546
|
+
while (queue.length) {
|
|
5547
|
+
if (++visits % 128 === 0) check();
|
|
5548
|
+
const current = pop();
|
|
5549
|
+
if (current.cost !== distances.get(current.id)) continue;
|
|
5550
|
+
if (current.id === to) {
|
|
5551
|
+
const result = [end];
|
|
5552
|
+
let id = to;
|
|
5553
|
+
while (id !== from) {
|
|
5554
|
+
id = previous.get(id);
|
|
5555
|
+
result.push(point(id));
|
|
5556
|
+
}
|
|
5557
|
+
return result.reverse();
|
|
5558
|
+
}
|
|
5559
|
+
const x = current.id % width, y = Math.floor(current.id / width);
|
|
5560
|
+
const neighbors = [
|
|
5561
|
+
x > 0 ? current.id - 1 : -1,
|
|
5562
|
+
x + 1 < width ? current.id + 1 : -1,
|
|
5563
|
+
y > 0 ? current.id - width : -1,
|
|
5564
|
+
y + 1 < ys.length ? current.id + width : -1
|
|
5565
|
+
];
|
|
5566
|
+
const a = point(current.id);
|
|
5567
|
+
for (const next of neighbors) {
|
|
5568
|
+
if (next < 0) continue;
|
|
5569
|
+
const b = point(next);
|
|
5570
|
+
if (obstacles.some((rect) => crossesRect(a, b, rect))) continue;
|
|
5571
|
+
const cost = current.cost + Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
5572
|
+
if (cost >= (distances.get(next) ?? Infinity)) continue;
|
|
5573
|
+
distances.set(next, cost);
|
|
5574
|
+
previous.set(next, current.id);
|
|
5575
|
+
push({
|
|
5576
|
+
id: next,
|
|
5577
|
+
cost
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
function routeOrthogonal(edge, source, target, direction, obstacles, waypoints, check) {
|
|
5583
|
+
const anchors = [
|
|
5584
|
+
endpoint(source, edge.sourcePort, direction, true),
|
|
5585
|
+
...waypoints,
|
|
5586
|
+
endpoint(target, edge.targetPort, direction, false)
|
|
5587
|
+
];
|
|
5588
|
+
const points = [];
|
|
5589
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
5590
|
+
const segment = connect(anchors[i - 1], anchors[i], obstacles, check);
|
|
5591
|
+
if (!segment) return void 0;
|
|
5592
|
+
points.push(...points.length ? segment.slice(1) : segment);
|
|
5593
|
+
}
|
|
5594
|
+
return points.filter((p, i) => {
|
|
5595
|
+
if (i === 0 || i === points.length - 1) return true;
|
|
5596
|
+
const before = points[i - 1], after = points[i + 1];
|
|
5597
|
+
return !(before.x === p.x && p.x === after.x && (p.y - before.y) * (after.y - p.y) >= 0 || before.y === p.y && p.y === after.y && (p.x - before.x) * (after.x - p.x) >= 0);
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
//#endregion
|
|
5602
|
+
//#region src/authoring/partial.ts
|
|
5603
|
+
const geometryKeys = [
|
|
5604
|
+
"x",
|
|
5605
|
+
"y",
|
|
5606
|
+
"width",
|
|
5607
|
+
"height"
|
|
5608
|
+
];
|
|
5609
|
+
function finite(value) {
|
|
5610
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
5611
|
+
}
|
|
5612
|
+
function samePosition(a, b) {
|
|
5613
|
+
return a.x === b.x && a.y === b.y;
|
|
5614
|
+
}
|
|
5615
|
+
function onRoute(point, points) {
|
|
5616
|
+
return points?.some((b, i) => {
|
|
5617
|
+
if (!i) return false;
|
|
5618
|
+
const a = points[i - 1];
|
|
5619
|
+
const cross = (point.x - a.x) * (b.y - a.y) - (point.y - a.y) * (b.x - a.x);
|
|
5620
|
+
return Math.abs(cross) < 1e-6 && point.x >= Math.min(a.x, b.x) - 1e-6 && point.x <= Math.max(a.x, b.x) + 1e-6 && point.y >= Math.min(a.y, b.y) - 1e-6 && point.y <= Math.max(a.y, b.y) + 1e-6;
|
|
5621
|
+
}) ?? false;
|
|
5622
|
+
}
|
|
5623
|
+
function labelReferences(constraints) {
|
|
5624
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5625
|
+
const add = (ref) => {
|
|
5626
|
+
if ("edgeId" in ref) ids.add(ref.edgeId);
|
|
5627
|
+
};
|
|
5628
|
+
for (const c$1 of constraints) if (c$1.kind === "align" || c$1.kind === "distribute") c$1.entities.forEach(add);
|
|
5629
|
+
else if (c$1.kind === "pin") add(c$1.entity);
|
|
5630
|
+
else if (c$1.kind === "linear") c$1.terms.forEach((term) => add(term.entity));
|
|
5631
|
+
return ids;
|
|
5632
|
+
}
|
|
5633
|
+
/** Authoring path: plan only selected nodes; route against the complete fixed scene. */
|
|
5634
|
+
function runPartialLayout(input, options, context, arrange, phase = "selection") {
|
|
5635
|
+
const scope = context.scope;
|
|
5636
|
+
if (scope.mode !== "partial" && scope.mode !== "route-only") throw new Error("Expected authoring scope");
|
|
5637
|
+
const selectedNodes = new Set(scope.mode === "partial" ? scope.nodeIds ?? [] : []);
|
|
5638
|
+
const selectedEdges = new Set(scope.mode === "route-only" ? scope.edgeIds ?? input.edges.map((e) => e.id) : scope.edgeIds ?? []);
|
|
5639
|
+
const geometry = scope.mode === "partial" ? scope.edgeGeometry ?? "both" : "routes";
|
|
5640
|
+
const routing = scope.mode === "partial" ? scope.routing ?? "affected" : "selected";
|
|
5641
|
+
const constraints = context.constraints ?? [];
|
|
5642
|
+
const constrainedLabels = labelReferences(constraints);
|
|
5643
|
+
for (const [ids, entities] of [[selectedNodes, input.nodes], [selectedEdges, input.edges]]) {
|
|
5644
|
+
const known = new Set(entities.map((e) => e.id));
|
|
5645
|
+
for (const id of ids) if (!known.has(id)) throw new LayoutError(`Unknown selected entity: ${id}`, "INVALID_SELECTION");
|
|
5646
|
+
}
|
|
5647
|
+
const containers = new Set(input.nodes.flatMap((n) => n.parentId == null ? [] : [n.parentId]));
|
|
5648
|
+
if ([...selectedNodes].some((id) => containers.has(id))) throw new UnsupportedLayoutError("Authoring layout cannot move containers; select leaf nodes or use full layout without geometry constraints");
|
|
5649
|
+
const previousNodes = new Map(scope.previous?.nodes.map((n) => [n.id, n]));
|
|
5650
|
+
const previousEdges = new Map(scope.previous?.edges.map((e) => [e.id, e]));
|
|
5651
|
+
const missing = (id) => {
|
|
5652
|
+
throw new LayoutError(`Fixed entity ${id} requires finite geometry in graph or previous`, "MISSING_GEOMETRY");
|
|
5653
|
+
};
|
|
5654
|
+
let nodes = input.nodes.map((node) => {
|
|
5655
|
+
const previous = previousNodes.get(node.id);
|
|
5656
|
+
const size = getNodeSize(node, options);
|
|
5657
|
+
const rect = { ...node };
|
|
5658
|
+
for (const key of geometryKeys) {
|
|
5659
|
+
const value = node[key] ?? previous?.[key];
|
|
5660
|
+
if (value !== void 0 && (!finite(value) || (key === "width" || key === "height") && value < 0)) missing(node.id);
|
|
5661
|
+
rect[key] = value ?? (selectedNodes.has(node.id) ? key === "width" || key === "height" ? size[key] : 0 : missing(node.id));
|
|
5662
|
+
if (!finite(rect[key]) || (key === "width" || key === "height") && rect[key] < 0) missing(node.id);
|
|
5663
|
+
}
|
|
5664
|
+
return rect;
|
|
5665
|
+
});
|
|
5666
|
+
let edges = input.edges.map((edge) => {
|
|
5667
|
+
const previous = previousEdges.get(edge.id);
|
|
5668
|
+
const absentLabel = !edge.label && (edge.width ?? previous?.width ?? 0) === 0 && (edge.height ?? previous?.height ?? 0) === 0;
|
|
5669
|
+
const rect = {
|
|
5670
|
+
...edge,
|
|
5671
|
+
...edge.points === void 0 && previous?.points !== void 0 ? { points: previous.points } : {},
|
|
5672
|
+
...edge.routing === void 0 && previous?.routing !== void 0 ? { routing: previous.routing } : {}
|
|
5673
|
+
};
|
|
5674
|
+
for (const key of geometryKeys) {
|
|
5675
|
+
const value = edge[key] ?? previous?.[key];
|
|
5676
|
+
if (value !== void 0 && (!finite(value) || (key === "width" || key === "height") && value < 0)) missing(edge.id);
|
|
5677
|
+
rect[key] = value ?? (selectedEdges.has(edge.id) && geometry !== "routes" && (key === "x" || key === "y") || absentLabel ? 0 : missing(edge.id));
|
|
5678
|
+
}
|
|
5679
|
+
return rect;
|
|
5680
|
+
});
|
|
5681
|
+
const authored = {
|
|
5682
|
+
...input,
|
|
5683
|
+
direction: options.direction ?? input.direction ?? scope.previous?.direction ?? "down",
|
|
5684
|
+
nodes,
|
|
5685
|
+
edges
|
|
5686
|
+
};
|
|
5687
|
+
const baseline = worldGeometry(authored);
|
|
5688
|
+
nodes = baseline.nodes;
|
|
5689
|
+
edges = baseline.edges;
|
|
5690
|
+
let graph = baseline;
|
|
5691
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
5692
|
+
const ancestorSets = /* @__PURE__ */ new Map();
|
|
5693
|
+
const ancestors = (id) => {
|
|
5694
|
+
const cached = ancestorSets.get(id);
|
|
5695
|
+
if (cached) return cached;
|
|
5696
|
+
const result = /* @__PURE__ */ new Set();
|
|
5697
|
+
let parent = byId.get(id)?.parentId;
|
|
5698
|
+
while (parent != null && !result.has(parent)) {
|
|
5699
|
+
result.add(parent);
|
|
5700
|
+
parent = byId.get(parent)?.parentId;
|
|
5701
|
+
}
|
|
5702
|
+
ancestorSets.set(id, result);
|
|
5703
|
+
return result;
|
|
5704
|
+
};
|
|
5705
|
+
for (const node of nodes) ancestors(node.id);
|
|
5706
|
+
const direction = options.direction ?? input.direction ?? scope.previous?.direction ?? "down";
|
|
5707
|
+
const gap = options.spacing?.node ?? 20;
|
|
5708
|
+
if (!finite(gap) || gap < 0) throw new LayoutError("Spacing must be finite and non-negative", "INVALID_OPTIONS");
|
|
5709
|
+
context.throwIfAborted();
|
|
5710
|
+
if (selectedNodes.size && phase === "selection") {
|
|
5711
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5712
|
+
for (const node of nodes) if (selectedNodes.has(node.id)) {
|
|
5713
|
+
const group = groups.get(node.parentId ?? null) ?? [];
|
|
5714
|
+
group.push(node);
|
|
5715
|
+
groups.set(node.parentId ?? null, group);
|
|
5716
|
+
}
|
|
5717
|
+
const placed = new Map(nodes.map((n) => [n.id, n]));
|
|
5718
|
+
for (const group of groups.values()) {
|
|
5719
|
+
const ids = new Set(group.map((n) => n.id));
|
|
5720
|
+
const local = arrange({
|
|
5721
|
+
...input,
|
|
5722
|
+
initialNodeId: void 0,
|
|
5723
|
+
nodes: group.map((n) => ({
|
|
5724
|
+
...n,
|
|
5725
|
+
parentId: void 0,
|
|
5726
|
+
initialNodeId: void 0
|
|
5727
|
+
})),
|
|
5728
|
+
edges: edges.filter((e) => ids.has(e.sourceId) && ids.has(e.targetId))
|
|
5729
|
+
}, {
|
|
5730
|
+
...options,
|
|
5731
|
+
padding: 0
|
|
5732
|
+
});
|
|
5733
|
+
const anchorX = Math.min(...group.map((n) => n.x));
|
|
5734
|
+
const anchorY = Math.min(...group.map((n) => n.y));
|
|
5735
|
+
const localX = Math.min(...local.nodes.map((n) => n.x));
|
|
5736
|
+
const localY = Math.min(...local.nodes.map((n) => n.y));
|
|
5737
|
+
const proposed = group.map((n) => {
|
|
5738
|
+
const p = local.nodes.find((item) => item.id === n.id);
|
|
5739
|
+
return {
|
|
5740
|
+
...n,
|
|
5741
|
+
x: p.x - localX + anchorX,
|
|
5742
|
+
y: p.y - localY + anchorY
|
|
5743
|
+
};
|
|
5744
|
+
});
|
|
5745
|
+
const obstacles = [...placed.values()].filter((n) => !ids.has(n.id));
|
|
5746
|
+
const fixedLabels = edges.filter((e) => geometry === "routes" || !selectedEdges.has(e.id));
|
|
5747
|
+
const candidates = [{
|
|
5748
|
+
x: 0,
|
|
5749
|
+
y: 0
|
|
5750
|
+
}];
|
|
5751
|
+
candidateGeneration: for (const obstacle of [...obstacles, ...fixedLabels]) for (const node of proposed) {
|
|
5752
|
+
context.throwIfAborted();
|
|
5753
|
+
if (candidates.length + 4 > 2e3) break candidateGeneration;
|
|
5754
|
+
candidates.push({
|
|
5755
|
+
x: obstacle.x + obstacle.width + gap - node.x,
|
|
5756
|
+
y: 0
|
|
5757
|
+
}, {
|
|
5758
|
+
x: obstacle.x - gap - node.x - node.width,
|
|
5759
|
+
y: 0
|
|
5760
|
+
}, {
|
|
5761
|
+
x: 0,
|
|
5762
|
+
y: obstacle.y + obstacle.height + gap - node.y
|
|
5763
|
+
}, {
|
|
5764
|
+
x: 0,
|
|
5765
|
+
y: obstacle.y - gap - node.y - node.height
|
|
5766
|
+
});
|
|
5767
|
+
}
|
|
5768
|
+
candidates.sort((a, b) => Math.abs(a.x) + Math.abs(a.y) - Math.abs(b.x) - Math.abs(b.y));
|
|
5769
|
+
let comparisons = 0;
|
|
5770
|
+
const budget = () => {
|
|
5771
|
+
if (++comparisons % 128 === 0) context.throwIfAborted();
|
|
5772
|
+
return comparisons <= 1e5;
|
|
5773
|
+
};
|
|
5774
|
+
const fits = (offset$1) => {
|
|
5775
|
+
context.throwIfAborted();
|
|
5776
|
+
return proposed.every((n) => {
|
|
5777
|
+
if (!budget()) return false;
|
|
5778
|
+
const translated = {
|
|
5779
|
+
...n,
|
|
5780
|
+
x: n.x + offset$1.x,
|
|
5781
|
+
y: n.y + offset$1.y
|
|
5782
|
+
};
|
|
5783
|
+
const parent = n.parentId == null ? void 0 : placed.get(n.parentId);
|
|
5784
|
+
if (parent && (translated.x < parent.x || translated.y < parent.y || translated.x + translated.width > parent.x + parent.width || translated.y + translated.height > parent.y + parent.height)) return false;
|
|
5785
|
+
return fixedLabels.every((label) => budget() && !overlaps(translated, label, gap)) && obstacles.every((o) => budget() && (ancestors(n.id).has(o.id) || ancestors(o.id).has(n.id) || !overlaps(translated, o, gap)));
|
|
5786
|
+
});
|
|
5787
|
+
};
|
|
5788
|
+
const offset = candidates.find((candidate) => comparisons <= 1e5 && fits(candidate));
|
|
5789
|
+
if (offset) for (const n of proposed) placed.set(n.id, {
|
|
5790
|
+
...n,
|
|
5791
|
+
x: n.x + offset.x,
|
|
5792
|
+
y: n.y + offset.y
|
|
5793
|
+
});
|
|
5794
|
+
else context.diagnostics.push({
|
|
5795
|
+
severity: "warning",
|
|
5796
|
+
code: "PLACEMENT_BLOCKED",
|
|
5797
|
+
message: "No placement found within the search budget without moving fixed geometry; preserved selected positions",
|
|
5798
|
+
entityIds: [...ids],
|
|
5799
|
+
geometry: "node"
|
|
5800
|
+
});
|
|
5801
|
+
}
|
|
5802
|
+
graph = {
|
|
5803
|
+
...graph,
|
|
5804
|
+
nodes: nodes.map((n) => placed.get(n.id))
|
|
5805
|
+
};
|
|
5806
|
+
}
|
|
5807
|
+
graph = solveGeometryConstraints(graph, constraints, selectedNodes, geometry === "routes" ? /* @__PURE__ */ new Set() : selectedEdges, context.diagnostics);
|
|
5808
|
+
const moved = graph.nodes.filter((n) => !samePosition(n, byId.get(n.id)));
|
|
5809
|
+
const movedIds = new Set(moved.map((n) => n.id));
|
|
5810
|
+
const resultNodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5811
|
+
const automatic = /* @__PURE__ */ new Set();
|
|
5812
|
+
const affected = (edge) => movedIds.has(edge.sourceId) || movedIds.has(edge.targetId) || moved.some((n) => !ancestors(edge.sourceId).has(n.id) && !ancestors(edge.targetId).has(n.id) && (routeCrosses(edge.points, n) || overlaps(edge, n) || edge.routing === "splines" && edge.points?.length && overlaps(n, {
|
|
5813
|
+
x: Math.min(...edge.points.map((p) => p.x)),
|
|
5814
|
+
y: Math.min(...edge.points.map((p) => p.y)),
|
|
5815
|
+
width: Math.max(...edge.points.map((p) => p.x)) - Math.min(...edge.points.map((p) => p.x)),
|
|
5816
|
+
height: Math.max(...edge.points.map((p) => p.y)) - Math.min(...edge.points.map((p) => p.y))
|
|
5817
|
+
})));
|
|
5818
|
+
const edgeById = new Map(graph.edges.map((e) => [e.id, e]));
|
|
5819
|
+
for (const constraint of constraints) if (constraint.kind === "waypoint") {
|
|
5820
|
+
if (!finite(constraint.point.x) || !finite(constraint.point.y) || !edgeById.has(constraint.edgeId)) throw new LayoutError(`${constraint.id}: invalid waypoint`, "INVALID_CONSTRAINT");
|
|
5821
|
+
}
|
|
5822
|
+
graph = {
|
|
5823
|
+
...graph,
|
|
5824
|
+
edges: graph.edges.map((edge) => {
|
|
5825
|
+
context.throwIfAborted();
|
|
5826
|
+
const needsRepair = affected(edge);
|
|
5827
|
+
const allowed = selectedEdges.has(edge.id) || routing === "affected" && needsRepair;
|
|
5828
|
+
const waypoints = constraints.filter((c$1) => c$1.kind === "waypoint" && c$1.edgeId === edge.id);
|
|
5829
|
+
const routeAllowed = phase === "constraints" ? needsRepair || waypoints.some((c$1) => c$1.kind === "waypoint" && (edge.routing === "splines" || !onRoute(c$1.point, edge.points))) : allowed && geometry !== "labels";
|
|
5830
|
+
const labelAllowed = allowed && geometry !== "routes" && !constrainedLabels.has(edge.id);
|
|
5831
|
+
const source = resultNodes.get(edge.sourceId);
|
|
5832
|
+
const target = resultNodes.get(edge.targetId);
|
|
5833
|
+
let next = edge;
|
|
5834
|
+
let routeFailed = false;
|
|
5835
|
+
if (needsRepair && !routeAllowed) context.diagnostics.push({
|
|
5836
|
+
severity: "warning",
|
|
5837
|
+
code: "ROUTE_REPAIR_REQUIRED",
|
|
5838
|
+
message: `Edge ${edge.id} needs routing outside the permitted geometry`,
|
|
5839
|
+
entityIds: [edge.id],
|
|
5840
|
+
geometry: "routes"
|
|
5841
|
+
});
|
|
5842
|
+
if (routeAllowed) {
|
|
5843
|
+
if (options.settings?.edgeRouting === "SPLINES" || options.strategies?.routeEdges) throw new UnsupportedLayoutError("Partial layout supports its built-in orthogonal router only");
|
|
5844
|
+
const ancestorIds = new Set([...ancestors(source.id), ...ancestors(target.id)]);
|
|
5845
|
+
const nodeObstacles = graph.nodes.filter((n) => !ancestorIds.has(n.id)).map((n) => n.id === source.id || n.id === target.id ? n : {
|
|
5846
|
+
...n,
|
|
5847
|
+
x: n.x - 1,
|
|
5848
|
+
y: n.y - 1,
|
|
5849
|
+
width: n.width + 2,
|
|
5850
|
+
height: n.height + 2
|
|
5851
|
+
});
|
|
5852
|
+
const labelObstacles = [...edgeById.values()].filter((e) => e.id !== edge.id && e.width > 0 && e.height > 0);
|
|
5853
|
+
const required = waypoints.filter((c$1) => (c$1.strength ?? "required") === "required");
|
|
5854
|
+
const accepted = new Set(required.map((c$1) => c$1.id));
|
|
5855
|
+
const route = () => routeOrthogonal(edge, source, target, direction, [...nodeObstacles, ...labelObstacles], waypoints.filter((c$1) => c$1.kind === "waypoint" && accepted.has(c$1.id)).map((c$1) => c$1.kind === "waypoint" ? c$1.point : {
|
|
5856
|
+
x: 0,
|
|
5857
|
+
y: 0
|
|
5858
|
+
}), () => context.throwIfAborted());
|
|
5859
|
+
let points = route();
|
|
5860
|
+
const priorities = {
|
|
5861
|
+
strong: 3,
|
|
5862
|
+
medium: 2,
|
|
5863
|
+
weak: 1,
|
|
5864
|
+
required: 4
|
|
5865
|
+
};
|
|
5866
|
+
for (const preference of waypoints.filter((c$1) => !accepted.has(c$1.id)).sort((a, b) => priorities[b.strength ?? "required"] - priorities[a.strength ?? "required"])) {
|
|
5867
|
+
accepted.add(preference.id);
|
|
5868
|
+
const candidate = route();
|
|
5869
|
+
if (candidate) points = candidate;
|
|
5870
|
+
else accepted.delete(preference.id);
|
|
5871
|
+
}
|
|
5872
|
+
if (points) next = {
|
|
5873
|
+
...next,
|
|
5874
|
+
points,
|
|
5875
|
+
routing: "orthogonal"
|
|
5876
|
+
};
|
|
5877
|
+
else {
|
|
5878
|
+
routeFailed = true;
|
|
5879
|
+
context.diagnostics.push({
|
|
5880
|
+
severity: "warning",
|
|
5881
|
+
code: "ROUTE_BLOCKED",
|
|
5882
|
+
message: `No route found for ${edge.id} within the search budget; preserved the route`,
|
|
5883
|
+
entityIds: [edge.id],
|
|
5884
|
+
geometry: "routes"
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
for (const constraint of waypoints) if (constraint.kind === "waypoint" && (routeFailed || next.routing === "splines" || !onRoute(constraint.point, next.points))) {
|
|
5889
|
+
if ((constraint.strength ?? "required") === "required") constraintFailure(constraint, "route cannot pass through the waypoint within the permitted geometry");
|
|
5890
|
+
context.diagnostics.push({
|
|
5891
|
+
severity: "warning",
|
|
5892
|
+
code: "CONSTRAINT_VIOLATION",
|
|
5893
|
+
message: `Waypoint ${constraint.id} could not be satisfied`,
|
|
5894
|
+
entityIds: [edge.id],
|
|
5895
|
+
constraintIds: [constraint.id],
|
|
5896
|
+
geometry: "routes"
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
5899
|
+
if (labelAllowed && (next.points?.length ?? 0) >= 2) {
|
|
5900
|
+
const midpoint = getPolylineMidpoint(next.points ?? []);
|
|
5901
|
+
const candidates = [
|
|
5902
|
+
{
|
|
5903
|
+
x: midpoint.x - next.width / 2,
|
|
5904
|
+
y: midpoint.y - next.height / 2
|
|
5905
|
+
},
|
|
5906
|
+
{
|
|
5907
|
+
x: midpoint.x - next.width / 2,
|
|
5908
|
+
y: midpoint.y - next.height - gap
|
|
5909
|
+
},
|
|
5910
|
+
{
|
|
5911
|
+
x: midpoint.x - next.width / 2,
|
|
5912
|
+
y: midpoint.y + gap
|
|
5913
|
+
},
|
|
5914
|
+
{
|
|
5915
|
+
x: midpoint.x + gap,
|
|
5916
|
+
y: midpoint.y - next.height / 2
|
|
5917
|
+
},
|
|
5918
|
+
{
|
|
5919
|
+
x: midpoint.x - next.width - gap,
|
|
5920
|
+
y: midpoint.y - next.height / 2
|
|
5921
|
+
}
|
|
5922
|
+
];
|
|
5923
|
+
const parentIds = new Set([...ancestors(source.id), ...ancestors(target.id)]);
|
|
5924
|
+
for (const obstacle of [...graph.nodes, ...edgeById.values()]) candidates.push({
|
|
5925
|
+
x: midpoint.x - next.width / 2,
|
|
5926
|
+
y: obstacle.y - next.height - gap
|
|
5927
|
+
}, {
|
|
5928
|
+
x: midpoint.x - next.width / 2,
|
|
5929
|
+
y: obstacle.y + obstacle.height + gap
|
|
5930
|
+
}, {
|
|
5931
|
+
x: obstacle.x - next.width - gap,
|
|
5932
|
+
y: midpoint.y - next.height / 2
|
|
5933
|
+
}, {
|
|
5934
|
+
x: obstacle.x + obstacle.width + gap,
|
|
5935
|
+
y: midpoint.y - next.height / 2
|
|
5936
|
+
});
|
|
5937
|
+
candidates.sort((a, b) => Math.hypot(a.x + next.width / 2 - midpoint.x, a.y + next.height / 2 - midpoint.y) - Math.hypot(b.x + next.width / 2 - midpoint.x, b.y + next.height / 2 - midpoint.y));
|
|
5938
|
+
const chosen = candidates.find((p) => {
|
|
5939
|
+
const rect = {
|
|
5940
|
+
...next,
|
|
5941
|
+
...p
|
|
5942
|
+
};
|
|
5943
|
+
return !graph.nodes.some((n) => !parentIds.has(n.id) && overlaps(rect, n)) && ![...edgeById.values()].some((e) => e.id !== edge.id && overlaps(rect, e));
|
|
5944
|
+
});
|
|
5945
|
+
if (chosen) next = {
|
|
5946
|
+
...next,
|
|
5947
|
+
...chosen
|
|
5948
|
+
};
|
|
5949
|
+
else context.diagnostics.push({
|
|
5950
|
+
severity: "warning",
|
|
5951
|
+
code: "LABEL_PLACEMENT_BLOCKED",
|
|
5952
|
+
message: `No clear label position found for ${edge.id}; preserved label position`,
|
|
5953
|
+
entityIds: [edge.id],
|
|
5954
|
+
geometry: "labels"
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
if (geometry === "labels" && allowed && !samePosition(next, baseline.edges.find((e) => e.id === edge.id))) context.diagnostics.push({
|
|
5958
|
+
severity: "info",
|
|
5959
|
+
code: "ROUTE_PRESERVED",
|
|
5960
|
+
message: `Route for ${edge.id} was preserved; label attachment may need repair`,
|
|
5961
|
+
entityIds: [edge.id],
|
|
5962
|
+
geometry: "routes"
|
|
5963
|
+
});
|
|
5964
|
+
edgeById.set(edge.id, next);
|
|
5965
|
+
if (!selectedEdges.has(edge.id) && (next.x !== edge.x || next.y !== edge.y || next.routing !== edge.routing || JSON.stringify(next.points) !== JSON.stringify(edge.points))) automatic.add(edge.id);
|
|
5966
|
+
return next;
|
|
5967
|
+
})
|
|
5968
|
+
};
|
|
5969
|
+
if (automatic.size) context.diagnostics.push({
|
|
5970
|
+
severity: "info",
|
|
5971
|
+
code: "AFFECTED_EDGES_UPDATED",
|
|
5972
|
+
message: "Repaired geometry affected by moved nodes",
|
|
5973
|
+
entityIds: [...automatic]
|
|
5974
|
+
});
|
|
5975
|
+
for (const node of graph.nodes) if (selectedNodes.has(node.id) || node.parentId != null && movedIds.has(node.parentId)) {
|
|
5976
|
+
const parent = node.parentId == null ? void 0 : resultNodes.get(node.parentId);
|
|
5977
|
+
if (parent && (node.x < parent.x || node.y < parent.y || node.x + node.width > parent.x + parent.width || node.y + node.height > parent.y + parent.height)) context.diagnostics.push({
|
|
5978
|
+
severity: "warning",
|
|
5979
|
+
code: "CONTAINMENT_VIOLATION",
|
|
5980
|
+
message: `Node ${node.id} extends outside its fixed parent`,
|
|
5981
|
+
entityIds: [node.id, parent.id],
|
|
5982
|
+
geometry: "node"
|
|
5983
|
+
});
|
|
5984
|
+
if (graph.nodes.some((other) => other.id !== node.id && !ancestors(node.id).has(other.id) && !ancestors(other.id).has(node.id) && overlaps(node, other))) context.diagnostics.push({
|
|
5985
|
+
severity: "warning",
|
|
5986
|
+
code: "NODE_OVERLAP",
|
|
5987
|
+
message: `Node ${node.id} overlaps another node`,
|
|
5988
|
+
entityIds: [node.id],
|
|
5989
|
+
geometry: "node"
|
|
5990
|
+
});
|
|
5991
|
+
}
|
|
5992
|
+
for (const edge of graph.edges) {
|
|
5993
|
+
const before = baseline.edges.find((e) => e.id === edge.id);
|
|
5994
|
+
if (!samePosition(edge, before)) {
|
|
5995
|
+
const containerIds = new Set([...ancestors(edge.sourceId), ...ancestors(edge.targetId)]);
|
|
5996
|
+
const collisions = [...graph.nodes.filter((n) => !containerIds.has(n.id) && overlaps(edge, n)), ...graph.edges.filter((other) => other.id !== edge.id && overlaps(edge, other))];
|
|
5997
|
+
if (collisions.length) context.diagnostics.push({
|
|
5998
|
+
severity: "warning",
|
|
5999
|
+
code: "LABEL_OVERLAP",
|
|
6000
|
+
message: `Label ${edge.id} overlaps fixed or constrained geometry`,
|
|
6001
|
+
entityIds: [edge.id, ...collisions.map((e) => e.id)],
|
|
6002
|
+
geometry: "labels"
|
|
6003
|
+
});
|
|
6004
|
+
for (const other of graph.edges) if (other.id !== edge.id && routeCrosses(other.points, edge) && !routeCrosses(other.points, before)) context.diagnostics.push({
|
|
6005
|
+
severity: "warning",
|
|
6006
|
+
code: "ROUTE_REPAIR_REQUIRED",
|
|
6007
|
+
message: `Route ${other.id} crosses the moved label ${edge.id}`,
|
|
6008
|
+
entityIds: [other.id, edge.id],
|
|
6009
|
+
geometry: "routes"
|
|
6010
|
+
});
|
|
6011
|
+
}
|
|
6012
|
+
if (geometry === "routes" && routeCrosses(before.points, before) && !routeCrosses(edge.points, edge)) context.diagnostics.push({
|
|
6013
|
+
severity: "warning",
|
|
6014
|
+
code: "LABEL_REPAIR_REQUIRED",
|
|
6015
|
+
message: `Label ${edge.id} is no longer attached to its route`,
|
|
6016
|
+
entityIds: [edge.id],
|
|
6017
|
+
geometry: "labels"
|
|
6018
|
+
});
|
|
6019
|
+
}
|
|
6020
|
+
return nativeGeometry(graph, authored);
|
|
6021
|
+
}
|
|
6022
|
+
|
|
3782
6023
|
//#endregion
|
|
3783
6024
|
//#region src/layered/separate-exterior-labels.ts
|
|
3784
6025
|
/** Assigns collision-free cross-axis lanes to inline labels and their route tracks. */
|
|
@@ -3790,7 +6031,7 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3790
6031
|
const flowEnd = (rect) => horizontalFlow ? rect.x + rect.width : rect.y + rect.height;
|
|
3791
6032
|
const crossStart = (rect) => horizontalFlow ? rect.y : rect.x;
|
|
3792
6033
|
const crossEnd = (rect) => horizontalFlow ? rect.y + rect.height : rect.x + rect.width;
|
|
3793
|
-
const overlaps = (left, right) => flowStart(left) < flowEnd(right) && flowEnd(left) > flowStart(right) && crossStart(left) < crossEnd(right) && crossEnd(left) > crossStart(right);
|
|
6034
|
+
const overlaps$1 = (left, right) => flowStart(left) < flowEnd(right) && flowEnd(left) > flowStart(right) && crossStart(left) < crossEnd(right) && crossEnd(left) > crossStart(right);
|
|
3794
6035
|
const labelRectByEdgeId = new Map(edges.filter((edge) => edge.width > 0 && edge.height > 0).map((edge) => [String(edge.id), {
|
|
3795
6036
|
x: edge.x,
|
|
3796
6037
|
y: edge.y,
|
|
@@ -3875,14 +6116,14 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3875
6116
|
x: labelRect.x + delta
|
|
3876
6117
|
};
|
|
3877
6118
|
const candidateTrackCross = exteriorTrack.cross + delta;
|
|
3878
|
-
return obstacles.every((obstacle) => !overlaps(candidateLabel, obstacle)) && nodeRects.every((nodeRect) => !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowStart) && !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowEnd));
|
|
6119
|
+
return obstacles.every((obstacle) => !overlaps$1(candidateLabel, obstacle)) && nodeRects.every((nodeRect) => !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowStart) && !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowEnd));
|
|
3879
6120
|
});
|
|
3880
6121
|
if (candidate === void 0) continue;
|
|
3881
6122
|
totalDelta = candidate - labelCrossStart;
|
|
3882
6123
|
if (horizontalFlow) labelRect.y += totalDelta;
|
|
3883
6124
|
else labelRect.x += totalDelta;
|
|
3884
6125
|
} else while (true) {
|
|
3885
|
-
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
6126
|
+
const blockers = obstacles.filter((rect) => overlaps$1(labelRect, rect));
|
|
3886
6127
|
if (blockers.length === 0) break;
|
|
3887
6128
|
const delta = (lowSide ? Math.min(...blockers.map(crossStart)) - spacing - (horizontalFlow ? edge.height : edge.width) : Math.max(...blockers.map(crossEnd)) + spacing) - crossStart(labelRect);
|
|
3888
6129
|
totalDelta += delta;
|
|
@@ -7804,7 +10045,7 @@ function adjustUnzippedSinkRoutes(graph, routes, direction, edgeNodeSpacing, edg
|
|
|
7804
10045
|
}
|
|
7805
10046
|
return result;
|
|
7806
10047
|
}
|
|
7807
|
-
function getNodeSize(node, options) {
|
|
10048
|
+
function getNodeSize$1(node, options) {
|
|
7808
10049
|
const measured = options.measure?.(node);
|
|
7809
10050
|
if (measured) return measured;
|
|
7810
10051
|
return {
|
|
@@ -7845,7 +10086,7 @@ function runWrappedPathPipeline(graph, options) {
|
|
|
7845
10086
|
currentId = targets[0];
|
|
7846
10087
|
}
|
|
7847
10088
|
if (orderedIds.length !== graph.nodes.length) return void 0;
|
|
7848
|
-
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
|
|
10089
|
+
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize$1(node, options)]));
|
|
7849
10090
|
const maximumWidth = Math.max(...[...sizes.values()].map((size) => size.width));
|
|
7850
10091
|
const maximumHeight = Math.max(...[...sizes.values()].map((size) => size.height));
|
|
7851
10092
|
if ([...sizes.values()].some((size) => size.width !== maximumWidth || size.height !== maximumHeight)) return;
|
|
@@ -8021,7 +10262,7 @@ function runWrappedMultiEdgePipeline(graph, options) {
|
|
|
8021
10262
|
}
|
|
8022
10263
|
if (nodeIdByRank.size !== maximumRank + 1) return void 0;
|
|
8023
10264
|
const orderedIds = Array.from({ length: maximumRank + 1 }, (_, index) => nodeIdByRank.get(index));
|
|
8024
|
-
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
|
|
10265
|
+
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize$1(node, options)]));
|
|
8025
10266
|
const layerSpacing = options.spacing?.layer ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
8026
10267
|
const nodeSpacing = options.spacing?.node ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
8027
10268
|
const widths = orderedIds.map((id) => sizes.get(id).width + layerSpacing);
|
|
@@ -8246,8 +10487,8 @@ function runCommentBoxPipeline(graph, options, context) {
|
|
|
8246
10487
|
const horizontal = direction === "left" || direction === "right";
|
|
8247
10488
|
const commentNodeSpacing = Number(options.settings?.["spacing.commentNode"] ?? 10);
|
|
8248
10489
|
const commentCommentSpacing = Number(options.settings?.["spacing.commentComment"] ?? 10);
|
|
8249
|
-
const targetSize = getNodeSize(target, options);
|
|
8250
|
-
const commentSizes = comments.map((comment) => getNodeSize(comment, options));
|
|
10490
|
+
const targetSize = getNodeSize$1(target, options);
|
|
10491
|
+
const commentSizes = comments.map((comment) => getNodeSize$1(comment, options));
|
|
8251
10492
|
const beforeIndexes = comments.flatMap((_, index) => index % 2 === 0 ? [index] : []);
|
|
8252
10493
|
const afterIndexes = comments.flatMap((_, index) => index % 2 === 1 ? [index] : []);
|
|
8253
10494
|
const rowFlowSize = (indexes) => indexes.reduce((sum, index) => sum + (horizontal ? commentSizes[index].width : commentSizes[index].height), 0) + Math.max(0, indexes.length - 1) * commentCommentSpacing;
|
|
@@ -8273,7 +10514,7 @@ function runCommentBoxPipeline(graph, options, context) {
|
|
|
8273
10514
|
edges: graph.edges.filter((edge) => !commentIds.has(edge.sourceId) && !commentIds.has(edge.targetId))
|
|
8274
10515
|
}, {
|
|
8275
10516
|
...options,
|
|
8276
|
-
measure: (node) => node.id === targetId ? groupSize : options.measure?.(node) ?? getNodeSize(node, options)
|
|
10517
|
+
measure: (node) => node.id === targetId ? groupSize : options.measure?.(node) ?? getNodeSize$1(node, options)
|
|
8277
10518
|
}, context);
|
|
8278
10519
|
const groupRect = base.nodes.find((node) => node.id === targetId);
|
|
8279
10520
|
if (!groupRect) return void 0;
|
|
@@ -8315,7 +10556,7 @@ function runCommentBoxPipeline(graph, options, context) {
|
|
|
8315
10556
|
x: horizontal ? point.x : point.x + crossShift,
|
|
8316
10557
|
y: horizontal ? point.y + crossShift : point.y
|
|
8317
10558
|
}));
|
|
8318
|
-
const endpoint = (source) => {
|
|
10559
|
+
const endpoint$1 = (source) => {
|
|
8319
10560
|
if (horizontal) return {
|
|
8320
10561
|
x: direction === "right" === source ? (targetRect.x ?? 0) + (targetRect.width ?? 0) : targetRect.x ?? 0,
|
|
8321
10562
|
y: (targetRect.y ?? 0) + (targetRect.height ?? 0) / 2
|
|
@@ -8325,8 +10566,8 @@ function runCommentBoxPipeline(graph, options, context) {
|
|
|
8325
10566
|
y: direction === "down" === source ? (targetRect.y ?? 0) + (targetRect.height ?? 0) : targetRect.y ?? 0
|
|
8326
10567
|
};
|
|
8327
10568
|
};
|
|
8328
|
-
if (edge.sourceId === targetId && points.length > 0) points[0] = endpoint(true);
|
|
8329
|
-
if (edge.targetId === targetId && points.length > 0) points[points.length - 1] = endpoint(false);
|
|
10569
|
+
if (edge.sourceId === targetId && points.length > 0) points[0] = endpoint$1(true);
|
|
10570
|
+
if (edge.targetId === targetId && points.length > 0) points[points.length - 1] = endpoint$1(false);
|
|
8330
10571
|
return [edge.id, {
|
|
8331
10572
|
...edge,
|
|
8332
10573
|
x: horizontal ? edge.x : (edge.x ?? 0) + crossShift,
|
|
@@ -8711,7 +10952,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8711
10952
|
}
|
|
8712
10953
|
const input = {
|
|
8713
10954
|
graph,
|
|
8714
|
-
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
|
|
10955
|
+
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize$1(node, options)])),
|
|
8715
10956
|
direction,
|
|
8716
10957
|
spacing: {
|
|
8717
10958
|
node: options.spacing?.node ?? options.settings?.["spacing.baseValue"] ?? 20,
|
|
@@ -9120,16 +11361,16 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9120
11361
|
const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
|
|
9121
11362
|
const shiftEndpoint = (endpointIndex, adjacentIndex, delta) => {
|
|
9122
11363
|
if (delta === 0) return;
|
|
9123
|
-
const endpoint = points[endpointIndex];
|
|
11364
|
+
const endpoint$1 = points[endpointIndex];
|
|
9124
11365
|
const adjacent = points[adjacentIndex];
|
|
9125
|
-
if (!endpoint) return;
|
|
9126
|
-
const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
|
|
11366
|
+
if (!endpoint$1) return;
|
|
11367
|
+
const originalCross = horizontalAntiparallelFlow ? endpoint$1.y : endpoint$1.x;
|
|
9127
11368
|
points[endpointIndex] = horizontalAntiparallelFlow ? {
|
|
9128
|
-
...endpoint,
|
|
9129
|
-
y: endpoint.y + delta
|
|
11369
|
+
...endpoint$1,
|
|
11370
|
+
y: endpoint$1.y + delta
|
|
9130
11371
|
} : {
|
|
9131
|
-
...endpoint,
|
|
9132
|
-
x: endpoint.x + delta
|
|
11372
|
+
...endpoint$1,
|
|
11373
|
+
x: endpoint$1.x + delta
|
|
9133
11374
|
};
|
|
9134
11375
|
if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontalAntiparallelFlow ? {
|
|
9135
11376
|
...adjacent,
|
|
@@ -9219,14 +11460,14 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9219
11460
|
for (const edge of [...incoming, ...outgoing]) {
|
|
9220
11461
|
const points = [...mutableRouteMap.get(edge.id) ?? []];
|
|
9221
11462
|
const endpointIndex = edge.sourceId === node.id ? 0 : points.length - 1;
|
|
9222
|
-
const endpoint = points[endpointIndex];
|
|
9223
|
-
if (!endpoint) continue;
|
|
11463
|
+
const endpoint$1 = points[endpointIndex];
|
|
11464
|
+
if (!endpoint$1) continue;
|
|
9224
11465
|
points[endpointIndex] = horizontal ? {
|
|
9225
|
-
...endpoint,
|
|
9226
|
-
x: endpoint.x + delta
|
|
11466
|
+
...endpoint$1,
|
|
11467
|
+
x: endpoint$1.x + delta
|
|
9227
11468
|
} : {
|
|
9228
|
-
...endpoint,
|
|
9229
|
-
y: endpoint.y + delta
|
|
11469
|
+
...endpoint$1,
|
|
11470
|
+
y: endpoint$1.y + delta
|
|
9230
11471
|
};
|
|
9231
11472
|
mutableRouteMap.set(edge.id, points);
|
|
9232
11473
|
}
|
|
@@ -9331,14 +11572,14 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9331
11572
|
if (points.length === 0) continue;
|
|
9332
11573
|
const index = edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : void 0;
|
|
9333
11574
|
if (index === void 0) continue;
|
|
9334
|
-
const endpoint = points[index];
|
|
9335
|
-
const originalCross = horizontal ? endpoint.y : endpoint.x;
|
|
11575
|
+
const endpoint$1 = points[index];
|
|
11576
|
+
const originalCross = horizontal ? endpoint$1.y : endpoint$1.x;
|
|
9336
11577
|
points[index] = horizontal ? {
|
|
9337
|
-
...endpoint,
|
|
9338
|
-
y: endpoint.y + delta
|
|
11578
|
+
...endpoint$1,
|
|
11579
|
+
y: endpoint$1.y + delta
|
|
9339
11580
|
} : {
|
|
9340
|
-
...endpoint,
|
|
9341
|
-
x: endpoint.x + delta
|
|
11581
|
+
...endpoint$1,
|
|
11582
|
+
x: endpoint$1.x + delta
|
|
9342
11583
|
};
|
|
9343
11584
|
const adjacentIndex = index === 0 ? 1 : points.length - 2;
|
|
9344
11585
|
const adjacent = points[adjacentIndex];
|
|
@@ -9543,15 +11784,27 @@ const layeredAlgorithm = {
|
|
|
9543
11784
|
capabilities: {
|
|
9544
11785
|
full: true,
|
|
9545
11786
|
incremental: false,
|
|
9546
|
-
partial:
|
|
9547
|
-
routeOnly:
|
|
11787
|
+
partial: true,
|
|
11788
|
+
routeOnly: true,
|
|
11789
|
+
constraints: true,
|
|
9548
11790
|
hierarchy: true,
|
|
9549
11791
|
ports: true
|
|
9550
11792
|
},
|
|
9551
11793
|
layout(graph, options, context) {
|
|
9552
|
-
return
|
|
11794
|
+
if (context.scope.mode === "partial" || context.scope.mode === "route-only") return context.measurePhase("partial-layout", () => runPartialLayout(graph, options ?? {}, context, getLayeredLayout));
|
|
11795
|
+
if (context.constraints?.length && graph.nodes.some((node) => node.parentId != null)) throw new UnsupportedLayoutError("Full layout with geometry constraints does not support containers; use partial leaf constraints or full layout without geometry constraints");
|
|
11796
|
+
const result = runLayeredPipeline(graph, options ?? {}, context);
|
|
11797
|
+
if (!context.constraints?.length) return result;
|
|
11798
|
+
return context.measurePhase("constraints", () => runPartialLayout(result, options ?? {}, {
|
|
11799
|
+
...context,
|
|
11800
|
+
scope: {
|
|
11801
|
+
mode: "partial",
|
|
11802
|
+
nodeIds: result.nodes.map((n) => n.id),
|
|
11803
|
+
edgeIds: [...labelReferences(context.constraints ?? [])]
|
|
11804
|
+
}
|
|
11805
|
+
}, getLayeredLayout, "constraints"));
|
|
9553
11806
|
}
|
|
9554
11807
|
};
|
|
9555
11808
|
|
|
9556
11809
|
//#endregion
|
|
9557
|
-
export {
|
|
11810
|
+
export { minimizeCrossingsInteractively as A, JavaRandom as B, breakCyclesGreedily as C, breakCyclesWithModelOrderBreadthFirstSearch as D, breakCyclesWithDepthFirstSearch as E, placeNodesInteractively as F, UnsupportedLayoutError as H, placePorts as I, routeEdgesOrthogonally as L, minimizeCrossingsWithMedian as M, minimizeCrossingsWithModelOrder as N, breakCyclesWithModelOrderDepthFirstSearch as O, placeNodesInLayers as P, routeEdgesWithPolylines as R, breakCyclesByStronglyConnectedNodeType as S, breakCyclesInteractively as T, LayoutError as V, assignLayersByLongestPathToSink as _, elkLayeredEnumValues as a, breakCyclesByModelOrder as b, placeNodesWithLinearSegments as c, assignLayersWithMinWidth as d, assignLayersWithNetworkSimplex as f, assignLayersByLongestPath as g, assignLayersByDepthFirstModelOrder as h, toElkLayeredOptions as i, minimizeCrossingsWithBarycenter as j, getPolylineMidpoint as k, placeNodesWithBrandesKoepf as l, assignLayersByBreadthFirstModelOrder as m, layeredAlgorithm as n, elkLayeredOptionDefinitions as o, c as p, fromElkLayeredOptionId as r, placeNodesWithNetworkSimplex as s, getLayeredLayout as t, assignLayersWithStretchWidth as u, assignLayersInteractively as v, breakCyclesGreedilyByModelOrder as w, breakCyclesByStronglyConnectedConnectivity as x, assignLayersWithCoffmanGraham as y, routeEdgesWithSplines as z };
|