@r0kshan/cairn 1.0.0-RC12 → 1.0.0-RC13
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/README.md +110 -11
- package/bin/cairn.mjs +450 -91
- package/dist/cairn.mjs +441 -88
- package/package.json +3 -3
package/bin/cairn.mjs
CHANGED
|
@@ -92256,10 +92256,13 @@ function lex(src, diagnostics) {
|
|
|
92256
92256
|
col++;
|
|
92257
92257
|
continue;
|
|
92258
92258
|
}
|
|
92259
|
-
|
|
92260
|
-
|
|
92261
|
-
|
|
92262
|
-
|
|
92259
|
+
const arrowGlyph = ["-->", "..>", "->"].find(
|
|
92260
|
+
(glyph) => src.startsWith(glyph, position) && glyph[0] === char
|
|
92261
|
+
);
|
|
92262
|
+
if (arrowGlyph) {
|
|
92263
|
+
push("arrow", arrowGlyph, line, col);
|
|
92264
|
+
position += arrowGlyph.length;
|
|
92265
|
+
col += arrowGlyph.length;
|
|
92263
92266
|
continue;
|
|
92264
92267
|
}
|
|
92265
92268
|
const singleCharKind = SINGLE_CHAR_TOKENS[char];
|
|
@@ -92317,10 +92320,11 @@ var init_lexer = __esm({
|
|
|
92317
92320
|
});
|
|
92318
92321
|
|
|
92319
92322
|
// src/models/ast.ts
|
|
92320
|
-
var defaultDiagramStyle, explanations;
|
|
92323
|
+
var ATTACH_SIDES, defaultDiagramStyle, explanations;
|
|
92321
92324
|
var init_ast = __esm({
|
|
92322
92325
|
"src/models/ast.ts"() {
|
|
92323
92326
|
"use strict";
|
|
92327
|
+
ATTACH_SIDES = ["left", "right", "top", "bottom"];
|
|
92324
92328
|
defaultDiagramStyle = () => ({
|
|
92325
92329
|
crossingHops: true,
|
|
92326
92330
|
compact: false,
|
|
@@ -92339,6 +92343,7 @@ var init_ast = __esm({
|
|
|
92339
92343
|
});
|
|
92340
92344
|
explanations = {
|
|
92341
92345
|
E0101: "Syntax error: the file does not follow the DSL grammar.",
|
|
92346
|
+
E0106: "`order:` takes a whole number \u2265 0. On a top-level element it sets the reading order the disposition defines \u2014 left to right for `wide`/`slide`, top to bottom for `tall`/`page` \u2014 by banding the element inside its own layout partition, so it never crosses into another partition. Inside a container the axis flips: a child's layer belongs to the flows and elk honors no constraint that would change it, so there `order:` sorts the siblings that share a layer. Values need not be contiguous.",
|
|
92342
92347
|
E0201: "Unknown element kind for the active view. Each view defines its own kinds (e.g. logical: actor-group, actor, system, layer, block, external).",
|
|
92343
92348
|
E0202: "Duplicate identifier. IDs are flat and unique per file (decision D1): every element needs a distinct ID so flows can reference it unambiguously.",
|
|
92344
92349
|
E0203: "The logical view forbids unlabelled arrows: every flow must name the data, command or event it carries. A logical diagram tells what circulates, not just who talks to whom.",
|
|
@@ -92355,6 +92360,7 @@ var init_ast = __esm({
|
|
|
92355
92360
|
E0215: "A deployed application (app-instance) must sit on a server or in a zone. It shows WHERE an application runs, without its internal detail (C4 deployment convention).",
|
|
92356
92361
|
E0216: "Network zones belong to a site (or nest inside a larger zone): DMZ and LAN only mean something relative to a perimeter.",
|
|
92357
92362
|
W0540: 'C4 container-diagram practice: inter-process relationships should be labelled with their technology/protocol ("the how, not just the what"). Human/actor interactions are exempt. Add `(API_REST, JSON)` after the label, or ignore if the diagram is intentionally functional-only.',
|
|
92363
|
+
E0223: "Unknown attachment side. A flow endpoint may name the side it attaches to \u2014 `APP.right -> DB.left` \u2014 using the diagram as it is read: `left`, `right`, `top`, `bottom`. The side is a hint, not a guarantee: a side the layout cannot reach is dropped with a W0570 warning rather than forced.",
|
|
92358
92364
|
E0240: "The infrastructure view requires every flow to carry its protocol (and port if relevant): the flow matrix is the primary output of this view. Add `(HTTPS/443)` after the label.",
|
|
92359
92365
|
E0221: 'Unknown business-object reference: a flow carries `[AN_ID]` that no `business-object` declaration defines. Declare it once (`business-object ID "Name" "description"`) so the registry and the chips stay consistent.',
|
|
92360
92366
|
E0222: "Business objects are a logical-view concept (what data circulates between functional blocks). They are not part of the application, infrastructure, or security views \u2014 model the exchange with the flow label and technical tail instead. Remove the `business-object` declaration and its `[refs]`, or switch the diagram to `logical`.",
|
|
@@ -92362,6 +92368,8 @@ var init_ast = __esm({
|
|
|
92362
92368
|
E0217: "A sensitive asset must sit inside a trust zone: its protection level is defined by the zone that contains it.",
|
|
92363
92369
|
E0218: "A security node (firewall, WAF, bastion, reverse proxy) lives inside a trust zone \u2014 typically the exposed zone whose traffic it filters.",
|
|
92364
92370
|
E0250: 'The security view requires each trust zone to declare a sensitivity level in parentheses after the label: `trust-zone DMZ "DMZ" (public)`. Levels, least \u2192 most trusted: public, internal, restricted, secret. The level drives the zone color and the trust-boundary crossing checks.',
|
|
92371
|
+
W0570: "A declared attachment side (`APP.right -> DB.left`) did not survive layout: the flow ended up on another side of the element. Pins are honored where the layout can reach them and dropped where it cannot, rather than forced into an unreadable route. Move the element instead (`order:`), pin the other endpoint, or drop the pin.",
|
|
92372
|
+
W0571: "An endpoint reads `ID.side`, but `ID.side` is itself a declared element, and a declared id always wins \u2014 so the flow attaches to that element and no side is pinned. Rename the element if you meant the side.",
|
|
92365
92373
|
W0560: "Security check: this flow enters a more-trusted zone from a less-trusted one without passing through a security-node (firewall/WAF/bastion). Route it through a filtering point, or confirm the direct path is deliberate. This is the diagram equivalent of a missing firewall rule review.",
|
|
92366
92374
|
W0561: "Security check: an inter-zone flow does not state its encryption/protocol. Cross-zone traffic should declare how it is protected: add `(TLS1.3)` or the relevant protocol after the label."
|
|
92367
92375
|
};
|
|
@@ -92998,9 +93006,9 @@ var init_element_tree = __esm({
|
|
|
92998
93006
|
// src/parser.ts
|
|
92999
93007
|
function parseFlow(p, sourceToken) {
|
|
93000
93008
|
const { matchToken, advance, reportError, lookAhead, syncToNextLine, model } = p;
|
|
93001
|
-
advance();
|
|
93009
|
+
const arrowToken = advance();
|
|
93002
93010
|
if (!matchToken("id")) {
|
|
93003
|
-
reportError(
|
|
93011
|
+
reportError(`target identifier expected after \`${arrowToken.text}\``, lookAhead().span);
|
|
93004
93012
|
syncToNextLine();
|
|
93005
93013
|
return;
|
|
93006
93014
|
}
|
|
@@ -93017,6 +93025,8 @@ function parseFlow(p, sourceToken) {
|
|
|
93017
93025
|
len: targetToken.span.col + targetToken.span.len - sourceToken.span.col
|
|
93018
93026
|
}
|
|
93019
93027
|
};
|
|
93028
|
+
if (arrowToken.text === "-->") flow.lineStyle = "dashed";
|
|
93029
|
+
else if (arrowToken.text === "..>") flow.lineStyle = "dotted";
|
|
93020
93030
|
if (matchToken("colon")) {
|
|
93021
93031
|
advance();
|
|
93022
93032
|
if (matchToken("str")) flow.label = advance().text;
|
|
@@ -93069,6 +93079,11 @@ function parseFlow(p, sourceToken) {
|
|
|
93069
93079
|
flow.style = {};
|
|
93070
93080
|
p.parseStyleEntries(null, flow.style);
|
|
93071
93081
|
}
|
|
93082
|
+
for (const split of [
|
|
93083
|
+
splitEndpoint(sourceToken, flow, "from"),
|
|
93084
|
+
splitEndpoint(targetToken, flow, "to")
|
|
93085
|
+
])
|
|
93086
|
+
if (split) p.endpointSplits.push(split);
|
|
93072
93087
|
model.flows.push(flow);
|
|
93073
93088
|
}
|
|
93074
93089
|
function parseElement(p, sourceToken, parent) {
|
|
@@ -93120,6 +93135,84 @@ function parseElement(p, sourceToken, parent) {
|
|
|
93120
93135
|
}
|
|
93121
93136
|
(parent ? parent.children : model.elements).push(element);
|
|
93122
93137
|
}
|
|
93138
|
+
function splitEndpoint(token, flow, role) {
|
|
93139
|
+
const dot = token.text.lastIndexOf(".");
|
|
93140
|
+
if (dot <= 0 || dot === token.text.length - 1) return null;
|
|
93141
|
+
const base = token.text.slice(0, dot);
|
|
93142
|
+
const side = token.text.slice(dot + 1);
|
|
93143
|
+
return {
|
|
93144
|
+
flow,
|
|
93145
|
+
role,
|
|
93146
|
+
raw: token.text,
|
|
93147
|
+
rawSpan: token.span,
|
|
93148
|
+
base,
|
|
93149
|
+
baseSpan: { line: token.span.line, col: token.span.col, len: base.length },
|
|
93150
|
+
side,
|
|
93151
|
+
sideSpan: { line: token.span.line, col: token.span.col + dot + 1, len: side.length }
|
|
93152
|
+
};
|
|
93153
|
+
}
|
|
93154
|
+
function resolveAttachSides(splits, model, diagnostics) {
|
|
93155
|
+
for (const split of splits) {
|
|
93156
|
+
if (model.index.has(split.raw)) {
|
|
93157
|
+
if (ATTACH_SIDES.includes(split.side))
|
|
93158
|
+
diagnostics.push({
|
|
93159
|
+
code: "W0571",
|
|
93160
|
+
severity: "warning",
|
|
93161
|
+
message: `\`${split.raw}\` is a declared element, so \`.${split.side}\` is not read as an attachment side`,
|
|
93162
|
+
span: split.rawSpan,
|
|
93163
|
+
note: "a declared id always wins over the `ID.side` reading",
|
|
93164
|
+
help: `rename the element if you meant to attach the flow to the ${split.side} side of \`${split.base}\``
|
|
93165
|
+
});
|
|
93166
|
+
continue;
|
|
93167
|
+
}
|
|
93168
|
+
if (!model.index.has(split.base)) continue;
|
|
93169
|
+
const known = ATTACH_SIDES.includes(split.side);
|
|
93170
|
+
if (!known)
|
|
93171
|
+
diagnostics.push({
|
|
93172
|
+
code: "E0223",
|
|
93173
|
+
severity: "error",
|
|
93174
|
+
message: `unknown attachment side \`${split.side}\``,
|
|
93175
|
+
span: split.sideSpan,
|
|
93176
|
+
note: "sides are named as the diagram is read",
|
|
93177
|
+
help: "use `left`, `right`, `top` or `bottom`, e.g. `APP.right -> DB.left`"
|
|
93178
|
+
});
|
|
93179
|
+
const side = known ? { value: split.side, span: split.sideSpan } : void 0;
|
|
93180
|
+
if (split.role === "from") {
|
|
93181
|
+
split.flow.from = split.base;
|
|
93182
|
+
split.flow.fromSpan = split.baseSpan;
|
|
93183
|
+
split.flow.fromSide = side;
|
|
93184
|
+
} else {
|
|
93185
|
+
split.flow.to = split.base;
|
|
93186
|
+
split.flow.toSpan = split.baseSpan;
|
|
93187
|
+
split.flow.toSide = side;
|
|
93188
|
+
}
|
|
93189
|
+
}
|
|
93190
|
+
}
|
|
93191
|
+
function tryOrderEntry(p, parent) {
|
|
93192
|
+
const { matchToken, advance, reportCoded, save, restore, syncToNextLine } = p;
|
|
93193
|
+
if (!parent || !matchToken("id", "order")) return false;
|
|
93194
|
+
const mark = save();
|
|
93195
|
+
const keyToken = advance();
|
|
93196
|
+
if (!matchToken("colon")) {
|
|
93197
|
+
restore(mark);
|
|
93198
|
+
return false;
|
|
93199
|
+
}
|
|
93200
|
+
advance();
|
|
93201
|
+
const valueToken = matchToken("num") ? advance() : null;
|
|
93202
|
+
const value = valueToken ? Number(valueToken.text) : Number.NaN;
|
|
93203
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
93204
|
+
reportCoded(
|
|
93205
|
+
"E0106",
|
|
93206
|
+
"`order` expects a whole number \u2265 0",
|
|
93207
|
+
(valueToken ?? keyToken).span,
|
|
93208
|
+
"e.g. `order: 1` \u2014 lower comes first in reading order"
|
|
93209
|
+
);
|
|
93210
|
+
syncToNextLine();
|
|
93211
|
+
return true;
|
|
93212
|
+
}
|
|
93213
|
+
parent.order = { value, span: valueToken.span };
|
|
93214
|
+
return true;
|
|
93215
|
+
}
|
|
93123
93216
|
function tryLegendBlock(p) {
|
|
93124
93217
|
const { matchToken, advance, reportError, lookAhead, skipNewlines, syncToNextLine, model } = p;
|
|
93125
93218
|
if (!matchToken("id", "legend")) return false;
|
|
@@ -93197,13 +93290,14 @@ function parse(src) {
|
|
|
93197
93290
|
const skipNewlines = () => {
|
|
93198
93291
|
while (matchToken("nl")) advance();
|
|
93199
93292
|
};
|
|
93200
|
-
const
|
|
93201
|
-
code
|
|
93293
|
+
const reportCoded = (code, message, span, help) => diagnostics.push({
|
|
93294
|
+
code,
|
|
93202
93295
|
severity: "error",
|
|
93203
93296
|
message,
|
|
93204
93297
|
span,
|
|
93205
93298
|
help
|
|
93206
93299
|
});
|
|
93300
|
+
const reportError = (message, span, help) => reportCoded("E0101", message, span, help);
|
|
93207
93301
|
const syncToNextLine = () => {
|
|
93208
93302
|
while (!matchToken("nl") && !matchToken("eof")) advance();
|
|
93209
93303
|
};
|
|
@@ -93216,6 +93310,7 @@ function parse(src) {
|
|
|
93216
93310
|
index: /* @__PURE__ */ new Map()
|
|
93217
93311
|
};
|
|
93218
93312
|
const flowSequence = { next: 0 };
|
|
93313
|
+
const endpointSplits = [];
|
|
93219
93314
|
skipNewlines();
|
|
93220
93315
|
if (matchToken("id", "diagram")) {
|
|
93221
93316
|
advance();
|
|
@@ -93307,12 +93402,14 @@ function parse(src) {
|
|
|
93307
93402
|
advance,
|
|
93308
93403
|
skipNewlines,
|
|
93309
93404
|
reportError,
|
|
93405
|
+
reportCoded,
|
|
93310
93406
|
syncToNextLine,
|
|
93311
93407
|
save: () => position,
|
|
93312
93408
|
restore: (mark) => {
|
|
93313
93409
|
position = mark;
|
|
93314
93410
|
},
|
|
93315
93411
|
flowSequence,
|
|
93412
|
+
endpointSplits,
|
|
93316
93413
|
parseStyleEntries,
|
|
93317
93414
|
parseElementBody
|
|
93318
93415
|
};
|
|
@@ -93322,6 +93419,7 @@ function parse(src) {
|
|
|
93322
93419
|
return;
|
|
93323
93420
|
}
|
|
93324
93421
|
if (tryStyleBlock(parent)) return;
|
|
93422
|
+
if (tryOrderEntry(parser, parent)) return;
|
|
93325
93423
|
if (!parent && tryLegendBlock(parser)) return;
|
|
93326
93424
|
if (!parent && tryBusinessObject(parser)) return;
|
|
93327
93425
|
parseFlowOrElement(parser, parent);
|
|
@@ -93334,6 +93432,7 @@ function parse(src) {
|
|
|
93334
93432
|
for (const [id, element] of indexElementsById(model.elements)) {
|
|
93335
93433
|
if (!model.index.has(id)) model.index.set(id, element);
|
|
93336
93434
|
}
|
|
93435
|
+
resolveAttachSides(endpointSplits, model, diagnostics);
|
|
93337
93436
|
return { model, diags: diagnostics };
|
|
93338
93437
|
}
|
|
93339
93438
|
function applyStyleEntry(entry) {
|
|
@@ -93693,12 +93792,27 @@ var init_views = __esm({
|
|
|
93693
93792
|
};
|
|
93694
93793
|
applicationView = {
|
|
93695
93794
|
name: "application",
|
|
93696
|
-
kinds: [
|
|
93697
|
-
|
|
93795
|
+
kinds: [
|
|
93796
|
+
"actor-group",
|
|
93797
|
+
"actor",
|
|
93798
|
+
"system",
|
|
93799
|
+
"application",
|
|
93800
|
+
"module",
|
|
93801
|
+
"queue",
|
|
93802
|
+
"datastore",
|
|
93803
|
+
"external"
|
|
93804
|
+
],
|
|
93805
|
+
containerKinds: ["actor-group", "system", "application", "external"],
|
|
93698
93806
|
// C4-style `(API_REST, JSON)`: the protocol half is worth tabulating, the port is not.
|
|
93699
|
-
|
|
93807
|
+
// `zoneOf` walks ancestors nearest-first, so an endpoint inside an application
|
|
93808
|
+
// still reads `Name (App)`; the system only annotates what sits directly in it.
|
|
93809
|
+
matrix: {
|
|
93810
|
+
zoneKinds: ["application", "system"],
|
|
93811
|
+
columns: ["num", "source", "dest", "proto", "nature"]
|
|
93812
|
+
},
|
|
93700
93813
|
partitions: {
|
|
93701
93814
|
"actor-group": 0,
|
|
93815
|
+
system: 1,
|
|
93702
93816
|
application: 1,
|
|
93703
93817
|
queue: 1,
|
|
93704
93818
|
datastore: 1,
|
|
@@ -93707,6 +93821,7 @@ var init_views = __esm({
|
|
|
93707
93821
|
legendNames: {
|
|
93708
93822
|
"actor-group": "Actor group",
|
|
93709
93823
|
actor: "Actor",
|
|
93824
|
+
system: "System",
|
|
93710
93825
|
application: "Application",
|
|
93711
93826
|
module: "Application module",
|
|
93712
93827
|
queue: "Message queue / broker",
|
|
@@ -93716,6 +93831,7 @@ var init_views = __esm({
|
|
|
93716
93831
|
legendNamesFr: {
|
|
93717
93832
|
"actor-group": "Groupe d'acteurs",
|
|
93718
93833
|
actor: "Acteur",
|
|
93834
|
+
system: "Syst\xE8me",
|
|
93719
93835
|
application: "Application",
|
|
93720
93836
|
module: "Module applicatif",
|
|
93721
93837
|
queue: "File de messages / broker",
|
|
@@ -93763,6 +93879,12 @@ var init_views = __esm({
|
|
|
93763
93879
|
fill: "#eef4fb",
|
|
93764
93880
|
stroke: { color: "#7a9cc4", style: "dashed", width: 1.2 }
|
|
93765
93881
|
},
|
|
93882
|
+
// Same palette as the logical view's `system`, so a system boundary reads
|
|
93883
|
+
// the same in both views.
|
|
93884
|
+
system: {
|
|
93885
|
+
fill: "#f6f2ea",
|
|
93886
|
+
stroke: { color: "#b09a6d", style: "solid", width: 1.2 }
|
|
93887
|
+
},
|
|
93766
93888
|
application: {
|
|
93767
93889
|
fill: "#e8f1f8",
|
|
93768
93890
|
stroke: { color: "#5b8db8", style: "solid", width: 1.2 }
|
|
@@ -93790,6 +93912,10 @@ var init_views = __esm({
|
|
|
93790
93912
|
fill: "#232a33",
|
|
93791
93913
|
stroke: { color: "#5c7fa8", style: "dashed", width: 1.2 }
|
|
93792
93914
|
},
|
|
93915
|
+
system: {
|
|
93916
|
+
fill: "#2b2822",
|
|
93917
|
+
stroke: { color: "#9c8558", style: "solid", width: 1.2 }
|
|
93918
|
+
},
|
|
93793
93919
|
application: {
|
|
93794
93920
|
fill: "#1f2a33",
|
|
93795
93921
|
stroke: { color: "#4a7ba6", style: "solid", width: 1.2 }
|
|
@@ -95266,7 +95392,7 @@ var init_slide_fold = __esm({
|
|
|
95266
95392
|
});
|
|
95267
95393
|
|
|
95268
95394
|
// src/geometry.ts
|
|
95269
|
-
var MIN_ATTACH_GAP, boxesOverlap, boxToSegmentSq, boxGapSq, boundsOf, pathLength, boxToPolylineSq;
|
|
95395
|
+
var MIN_ATTACH_GAP, boxesOverlap, boxToSegmentSq, boxGapSq, boundsOf, pathLength, DETOUR_RATIO, DETOUR_WASTE, isLongDetour, boxToPolylineSq;
|
|
95270
95396
|
var init_geometry = __esm({
|
|
95271
95397
|
"src/geometry.ts"() {
|
|
95272
95398
|
"use strict";
|
|
@@ -95301,6 +95427,14 @@ var init_geometry = __esm({
|
|
|
95301
95427
|
length += Math.abs(points[index + 1].x - points[index].x) + Math.abs(points[index + 1].y - points[index].y);
|
|
95302
95428
|
return length;
|
|
95303
95429
|
};
|
|
95430
|
+
DETOUR_RATIO = 2.2;
|
|
95431
|
+
DETOUR_WASTE = 400;
|
|
95432
|
+
isLongDetour = (points, boxA, boxB) => {
|
|
95433
|
+
const direct = Math.abs(boxA.x + boxA.width / 2 - (boxB.x + boxB.width / 2)) + Math.abs(boxA.y + boxA.height / 2 - (boxB.y + boxB.height / 2));
|
|
95434
|
+
if (direct <= 0) return false;
|
|
95435
|
+
const length = pathLength(points);
|
|
95436
|
+
return length > DETOUR_RATIO * direct && length - direct > DETOUR_WASTE;
|
|
95437
|
+
};
|
|
95304
95438
|
boxToPolylineSq = (box, points) => {
|
|
95305
95439
|
let best = Number.POSITIVE_INFINITY;
|
|
95306
95440
|
for (let index = 0; index + 1 < points.length; index++) {
|
|
@@ -95330,6 +95464,7 @@ function detourCandidates(scene, model) {
|
|
|
95330
95464
|
for (const edge of scene.edges) {
|
|
95331
95465
|
const flow = flowById.get(edge.id);
|
|
95332
95466
|
if (!flow || edge.pts.length < 2) continue;
|
|
95467
|
+
if (edge.pinned) continue;
|
|
95333
95468
|
const source = nodeById.get(flow.from);
|
|
95334
95469
|
const target = nodeById.get(flow.to);
|
|
95335
95470
|
if (!source || !target) continue;
|
|
@@ -95750,9 +95885,7 @@ function laneOffsets(lc, subset, direction, anchor) {
|
|
|
95750
95885
|
const ownPosition = spanAnchor(lc.scene, { left, right, exempt }, direction, anchor) + direction * CHANNEL_GAP;
|
|
95751
95886
|
const start = lane === 0 ? ownPosition : direction > 0 ? Math.max(ownPosition, positions[lane - 1] + labelHeights[lane - 1] + 14) : Math.min(ownPosition, positions[lane - 1] - (labelHeights[lane - 1] + 14));
|
|
95752
95887
|
labelHeights.push(labelHeight);
|
|
95753
|
-
positions.push(
|
|
95754
|
-
resolveLanePosition(lc, { left, right, exempt, labelHeight, start }, direction)
|
|
95755
|
-
);
|
|
95888
|
+
positions.push(resolveLanePosition(lc, { left, right, exempt, labelHeight, start }, direction));
|
|
95756
95889
|
}
|
|
95757
95890
|
return positions;
|
|
95758
95891
|
}
|
|
@@ -95943,7 +96076,9 @@ var init_route_detour = __esm({
|
|
|
95943
96076
|
blockedBelowBy = (leafBoxes, x, top) => leafBoxes.some(
|
|
95944
96077
|
(node) => x >= node.x - 2 && x <= node.x + node.width + 2 && node.y + node.height > top + 1
|
|
95945
96078
|
);
|
|
95946
|
-
blockedAboveBy = (leafBoxes, titleBoxes, x, bottom) => leafBoxes.some(
|
|
96079
|
+
blockedAboveBy = (leafBoxes, titleBoxes, x, bottom) => leafBoxes.some(
|
|
96080
|
+
(node) => x >= node.x - 2 && x <= node.x + node.width + 2 && node.y < bottom - 1
|
|
96081
|
+
) || titleBoxes.some(
|
|
95947
96082
|
(box) => x >= box.x - TITLE_CLEARANCE && x <= box.x + box.width + TITLE_CLEARANCE && box.y < bottom - 1
|
|
95948
96083
|
);
|
|
95949
96084
|
horizontalBlocked = (leafBoxes, y, probe) => leafBoxes.some(
|
|
@@ -97224,6 +97359,7 @@ function clearRunHug(ctx, edge, run, own) {
|
|
|
97224
97359
|
if (attemptHugFix(ctx, edge, { run, axis, target }, own)) return true;
|
|
97225
97360
|
}
|
|
97226
97361
|
if (run.i !== 0 && run.i + 1 !== edge.pts.length - 1) return false;
|
|
97362
|
+
if (edge.pinned?.[run.i === 0 ? "start" : "end"]) return false;
|
|
97227
97363
|
if (!sideOf(edge.pts[run.i === 0 ? 0 : edge.pts.length - 1], leaves)) return false;
|
|
97228
97364
|
return resideAtSide(ctx, edge, run, own);
|
|
97229
97365
|
}
|
|
@@ -97640,8 +97776,10 @@ function generateReaimCandidates(AWAY_TOL2, from, to) {
|
|
|
97640
97776
|
};
|
|
97641
97777
|
const normalOf = (side) => side === "north" ? { x: 0, y: -1 } : side === "south" ? { x: 0, y: 1 } : side === "west" ? { x: -1, y: 0 } : { x: 1, y: 0 };
|
|
97642
97778
|
const candidates = [];
|
|
97643
|
-
|
|
97644
|
-
|
|
97779
|
+
const srcSides = from.keepSide ? [from.keepSide] : facingSides(src, dstC);
|
|
97780
|
+
const dstSides = to.keepSide ? [to.keepSide] : facingSides(dst, srcC);
|
|
97781
|
+
for (const srcSide of srcSides)
|
|
97782
|
+
for (const dstSide of dstSides) {
|
|
97645
97783
|
const na = normalOf(srcSide);
|
|
97646
97784
|
const nb = normalOf(dstSide);
|
|
97647
97785
|
const horizontalA = na.x !== 0;
|
|
@@ -97785,8 +97923,8 @@ function reaimEdge(rctx, edge) {
|
|
|
97785
97923
|
const dstC = { x: dst.x + dst.width / 2, y: dst.y + dst.height / 2 };
|
|
97786
97924
|
const candidates = generateReaimCandidates(
|
|
97787
97925
|
AWAY_TOL2,
|
|
97788
|
-
{ node: src, centre: srcC },
|
|
97789
|
-
{ node: dst, centre: dstC }
|
|
97926
|
+
{ node: src, centre: srcC, keepSide: edge.pinned?.start ? srcSeat.side : null },
|
|
97927
|
+
{ node: dst, centre: dstC, keepSide: edge.pinned?.end ? dstSeat.side : null }
|
|
97790
97928
|
);
|
|
97791
97929
|
const outside = runsExcept(edge.id);
|
|
97792
97930
|
const labelBoxes = seatedLabelBoxes(edge);
|
|
@@ -97818,7 +97956,8 @@ function reaimEdge(rctx, edge) {
|
|
|
97818
97956
|
function reaimWrapAroundTerminals(ctx) {
|
|
97819
97957
|
const { scene } = ctx;
|
|
97820
97958
|
const rctx = createReaimContext(ctx);
|
|
97821
|
-
for (const edge of scene.edges)
|
|
97959
|
+
for (const edge of scene.edges)
|
|
97960
|
+
if (!(edge.pinned?.start && edge.pinned?.end)) reaimEdge(rctx, edge);
|
|
97822
97961
|
}
|
|
97823
97962
|
function shiftCoincidentRun(leaves, pts, b, newAt) {
|
|
97824
97963
|
if (b.vert) {
|
|
@@ -98264,8 +98403,10 @@ function bestUnweaveRoute(uctx, job) {
|
|
|
98264
98403
|
const { edge, srcSeat, dstSeat } = job;
|
|
98265
98404
|
let best = null;
|
|
98266
98405
|
let bestLength = Number.POSITIVE_INFINITY;
|
|
98267
|
-
|
|
98268
|
-
|
|
98406
|
+
const srcSides = edge.pinned?.start ? [srcSeat.side] : ALL_SIDES;
|
|
98407
|
+
const dstSides = edge.pinned?.end ? [dstSeat.side] : ALL_SIDES;
|
|
98408
|
+
for (const srcSide of srcSides)
|
|
98409
|
+
for (const dstSide of dstSides)
|
|
98269
98410
|
for (const srcOffset of SEAT_OFFSETS)
|
|
98270
98411
|
for (const dstOffset of SEAT_OFFSETS) {
|
|
98271
98412
|
const a = unweaveSeat(srcSeat.node, srcSide, srcOffset);
|
|
@@ -98318,6 +98459,7 @@ function unweaveAndClearContainers(ctx) {
|
|
|
98318
98459
|
for (const edge of [...scene.edges].sort(
|
|
98319
98460
|
(a, b) => (Number.parseInt(a.id.slice(1), 10) || 0) - (Number.parseInt(b.id.slice(1), 10) || 0)
|
|
98320
98461
|
)) {
|
|
98462
|
+
if (edge.pinned?.start && edge.pinned?.end) continue;
|
|
98321
98463
|
unweaveEdge(uctx, edge);
|
|
98322
98464
|
}
|
|
98323
98465
|
}
|
|
@@ -98618,7 +98760,7 @@ function createLaneModel(deps) {
|
|
|
98618
98760
|
const fromB = lane - (firstVertical ? pts[3].y : pts[3].x);
|
|
98619
98761
|
return fromA * fromB > 0;
|
|
98620
98762
|
};
|
|
98621
|
-
return { channelU, isChannelU };
|
|
98763
|
+
return { channelU, isChannelU, laneBeyond };
|
|
98622
98764
|
}
|
|
98623
98765
|
function optimiseRoutes(scene, titleBoxes = [], folded = false) {
|
|
98624
98766
|
const leaves = scene.nodes.filter((node) => !node.container);
|
|
@@ -98628,7 +98770,7 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
|
|
|
98628
98770
|
const preRouted = new Set(scene.edges.filter((edge) => edge.detour).map((edge) => edge.id));
|
|
98629
98771
|
const { SIDES, seatOffsetsFor, seatOn, outward, horizontalSide } = createSeatModel(titleBoxes);
|
|
98630
98772
|
const clock = { generation: 0 };
|
|
98631
|
-
const { channelU, isChannelU } = createLaneModel({
|
|
98773
|
+
const { channelU, isChannelU, laneBeyond } = createLaneModel({
|
|
98632
98774
|
scene,
|
|
98633
98775
|
inspector,
|
|
98634
98776
|
titleBoxes,
|
|
@@ -98638,7 +98780,10 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
|
|
|
98638
98780
|
const shapesFor = (a, b, sides, subject) => {
|
|
98639
98781
|
const aH = horizontalSide(sides.a);
|
|
98640
98782
|
const bH = horizontalSide(sides.b);
|
|
98641
|
-
if (aH !== bH)
|
|
98783
|
+
if (aH !== bH) {
|
|
98784
|
+
const plainL = [a, aH ? { x: b.x, y: a.y } : { x: a.x, y: b.y }, b];
|
|
98785
|
+
return [plainL, ...approachLanes(a, b, sides, subject)];
|
|
98786
|
+
}
|
|
98642
98787
|
if (outward(sides.a) === outward(sides.b)) return channelU(a, b, sides.a, subject);
|
|
98643
98788
|
if (aH)
|
|
98644
98789
|
return [
|
|
@@ -98652,6 +98797,40 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
|
|
|
98652
98797
|
Math.max(a.y, b.y) - CHANNEL_INSET
|
|
98653
98798
|
].map((mid) => [a, { x: a.x, y: mid }, { x: b.x, y: mid }, b]);
|
|
98654
98799
|
};
|
|
98800
|
+
const approachLanes = (a, b, sides, subject) => {
|
|
98801
|
+
if (!subject.edge.pinned) return [];
|
|
98802
|
+
const aH = horizontalSide(sides.a);
|
|
98803
|
+
const bH = horizontalSide(sides.b);
|
|
98804
|
+
const beforeA = sides.a === "west" || sides.a === "north";
|
|
98805
|
+
const crossLane = laneBeyond(
|
|
98806
|
+
{
|
|
98807
|
+
spanLo: Math.min(a.x, b.x),
|
|
98808
|
+
spanHi: Math.max(a.x, b.x),
|
|
98809
|
+
start: (aH ? a.x : a.y) + (beforeA ? -CHANNEL_STEP : CHANNEL_STEP),
|
|
98810
|
+
vertical: !aH,
|
|
98811
|
+
before: beforeA
|
|
98812
|
+
},
|
|
98813
|
+
subject,
|
|
98814
|
+
true
|
|
98815
|
+
);
|
|
98816
|
+
if (crossLane === null) return [];
|
|
98817
|
+
const beforeB = sides.b === "west" || sides.b === "north";
|
|
98818
|
+
const search = {
|
|
98819
|
+
spanLo: bH ? Math.min(crossLane, b.y) : Math.min(crossLane, b.x),
|
|
98820
|
+
spanHi: bH ? Math.max(crossLane, b.y) : Math.max(crossLane, b.x),
|
|
98821
|
+
start: (bH ? b.x : b.y) + (beforeB ? -CHANNEL_STEP : CHANNEL_STEP),
|
|
98822
|
+
// `laneBeyond` names the axis after the terminal legs: a lane at a
|
|
98823
|
+
// constant x is the `vertical: false` search.
|
|
98824
|
+
vertical: !bH,
|
|
98825
|
+
before: beforeB
|
|
98826
|
+
};
|
|
98827
|
+
const lanes = [laneBeyond(search, subject, false), laneBeyond(search, subject, true)].filter(
|
|
98828
|
+
(lane, index, all) => lane !== null && all.indexOf(lane) === index
|
|
98829
|
+
);
|
|
98830
|
+
return lanes.map(
|
|
98831
|
+
(lane) => bH ? [a, { x: a.x, y: crossLane }, { x: lane, y: crossLane }, { x: lane, y: b.y }, b] : [a, { x: crossLane, y: a.y }, { x: crossLane, y: lane }, { x: b.x, y: lane }, b]
|
|
98832
|
+
);
|
|
98833
|
+
};
|
|
98655
98834
|
const routeCache = /* @__PURE__ */ new Map();
|
|
98656
98835
|
const routesFor = (edge) => {
|
|
98657
98836
|
const cacheKey = `${clock.generation}|${edge.id}|${edge.pts[0].x},${edge.pts[0].y}|${edge.pts[edge.pts.length - 1].x},${edge.pts[edge.pts.length - 1].y}`;
|
|
@@ -98662,8 +98841,10 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
|
|
|
98662
98841
|
const subject = { ends, edge };
|
|
98663
98842
|
const out = [];
|
|
98664
98843
|
const seen = /* @__PURE__ */ new Set();
|
|
98665
|
-
|
|
98666
|
-
|
|
98844
|
+
const aSides = edge.pinned?.start ? [ends[0].side] : SIDES;
|
|
98845
|
+
const bSides = edge.pinned?.end ? [ends[1].side] : SIDES;
|
|
98846
|
+
for (const aSide of aSides)
|
|
98847
|
+
for (const bSide of bSides) {
|
|
98667
98848
|
const sides = { a: aSide, b: bSide };
|
|
98668
98849
|
for (const aOff of seatOffsetsFor(ends[0].node, aSide))
|
|
98669
98850
|
for (const bOff of seatOffsetsFor(ends[1].node, bSide))
|
|
@@ -99052,13 +99233,16 @@ function computeIngressExternalElements(model) {
|
|
|
99052
99233
|
}
|
|
99053
99234
|
return ingressExternalElements;
|
|
99054
99235
|
}
|
|
99055
|
-
function toElkNode2(element, compact,
|
|
99236
|
+
function toElkNode2(element, compact, fonts, root = false) {
|
|
99237
|
+
const { cont: containerFontSize, node: nodeFontSize } = fonts;
|
|
99056
99238
|
if (element.children.length) {
|
|
99057
99239
|
const lineCount = (element.label ?? element.id).split("\n").length;
|
|
99058
99240
|
return {
|
|
99059
99241
|
id: element.id,
|
|
99060
99242
|
layoutOptions: {
|
|
99061
|
-
"elk.padding": `[top=${(compact ? 11 : 13) + lineCount * 14},left=${compact ? 7 : 9},bottom=${compact ? 7 : 9},right=${compact ? 7 : 9}]
|
|
99243
|
+
"elk.padding": `[top=${(compact ? 11 : 13) + lineCount * 14},left=${compact ? 7 : 9},bottom=${compact ? 7 : 9},right=${compact ? 7 : 9}]`,
|
|
99244
|
+
...root ? {} : orderOption(element),
|
|
99245
|
+
...semiInteractiveOption(element.children)
|
|
99062
99246
|
},
|
|
99063
99247
|
labels: [
|
|
99064
99248
|
{
|
|
@@ -99067,7 +99251,7 @@ function toElkNode2(element, compact, containerFontSize, nodeFontSize) {
|
|
|
99067
99251
|
}
|
|
99068
99252
|
],
|
|
99069
99253
|
children: element.children.map(
|
|
99070
|
-
(child) => toElkNode2(child, compact,
|
|
99254
|
+
(child) => toElkNode2(child, compact, fonts)
|
|
99071
99255
|
)
|
|
99072
99256
|
};
|
|
99073
99257
|
}
|
|
@@ -99075,6 +99259,7 @@ function toElkNode2(element, compact, containerFontSize, nodeFontSize) {
|
|
|
99075
99259
|
const isActor = element.kind === "actor";
|
|
99076
99260
|
return {
|
|
99077
99261
|
id: element.id,
|
|
99262
|
+
...element.order && !root ? { layoutOptions: orderOption(element) } : {},
|
|
99078
99263
|
width: isActor ? Math.max(64, measure(element.label ?? element.id, nodeFontSize - 1.5).width + 8) : Math.max(compact ? 98 : 108, measured.width + (compact ? 10 : 12)),
|
|
99079
99264
|
height: isActor ? 54 + ((element.label ?? element.id).split("\n").length - 1) * 11 : Math.max(compact ? 36 : 38, measured.height + (compact ? 10 : 12))
|
|
99080
99265
|
};
|
|
@@ -99161,6 +99346,11 @@ function transpose(scene, titleBoxes) {
|
|
|
99161
99346
|
}
|
|
99162
99347
|
[scene.width, scene.height] = [scene.height, scene.width];
|
|
99163
99348
|
}
|
|
99349
|
+
function beatsRelayout(round, best) {
|
|
99350
|
+
if (!best) return true;
|
|
99351
|
+
if (round.tier !== best.tier) return round.tier < best.tier;
|
|
99352
|
+
return round.detours < best.detours;
|
|
99353
|
+
}
|
|
99164
99354
|
function attachAwayOf(scene, model) {
|
|
99165
99355
|
const ATTACH_AWAY_TOL = 24;
|
|
99166
99356
|
const byId = new Map(scene.nodes.map((node) => [node.id, node]));
|
|
@@ -99171,21 +99361,35 @@ function attachAwayOf(scene, model) {
|
|
|
99171
99361
|
const from = byId.get(flow?.from ?? "");
|
|
99172
99362
|
const to = byId.get(flow?.to ?? "");
|
|
99173
99363
|
if (!from || !to) continue;
|
|
99174
|
-
const
|
|
99364
|
+
const centerOf2 = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
|
|
99175
99365
|
const away = (seg, target) => Math.abs(seg.x) >= 0.5 && Math.abs(target.x) > ATTACH_AWAY_TOL && seg.x * target.x < 0 || Math.abs(seg.y) >= 0.5 && Math.abs(target.y) > ATTACH_AWAY_TOL && seg.y * target.y < 0;
|
|
99176
99366
|
const p0 = e.pts[0];
|
|
99177
99367
|
const p1 = e.pts[1];
|
|
99178
99368
|
const pn = e.pts[e.pts.length - 1];
|
|
99179
99369
|
const pm = e.pts[e.pts.length - 2];
|
|
99180
|
-
const toCenter =
|
|
99181
|
-
const fromCenter =
|
|
99182
|
-
if (away({ x: p1.x - p0.x, y: p1.y - p0.y }, { x: toCenter.x - p0.x, y: toCenter.y - p0.y }))
|
|
99370
|
+
const toCenter = centerOf2(to);
|
|
99371
|
+
const fromCenter = centerOf2(from);
|
|
99372
|
+
if (!e.pinned?.start && away({ x: p1.x - p0.x, y: p1.y - p0.y }, { x: toCenter.x - p0.x, y: toCenter.y - p0.y }))
|
|
99183
99373
|
flagged.add(e.id);
|
|
99184
|
-
if (away({ x: pn.x - pm.x, y: pn.y - pm.y }, { x: pn.x - fromCenter.x, y: pn.y - fromCenter.y }))
|
|
99374
|
+
if (!e.pinned?.end && away({ x: pn.x - pm.x, y: pn.y - pm.y }, { x: pn.x - fromCenter.x, y: pn.y - fromCenter.y }))
|
|
99185
99375
|
flagged.add(e.id);
|
|
99376
|
+
if (!e.pinned && isLongDetour(e.pts, from, to)) flagged.add(e.id);
|
|
99186
99377
|
}
|
|
99187
99378
|
return flagged;
|
|
99188
99379
|
}
|
|
99380
|
+
function longDetourCount(scene, model) {
|
|
99381
|
+
const byId = new Map(scene.nodes.map((node) => [node.id, node]));
|
|
99382
|
+
let count = 0;
|
|
99383
|
+
for (const edge of scene.edges) {
|
|
99384
|
+
if (edge.pts.length < 2) continue;
|
|
99385
|
+
const flow = model.flows.find((f) => f.id === edge.id);
|
|
99386
|
+
const from = byId.get(flow?.from ?? "");
|
|
99387
|
+
const to = byId.get(flow?.to ?? "");
|
|
99388
|
+
if (!from || !to) continue;
|
|
99389
|
+
if (isLongDetour(edge.pts, from, to)) count++;
|
|
99390
|
+
}
|
|
99391
|
+
return count;
|
|
99392
|
+
}
|
|
99189
99393
|
function selectionExtras(scene, model) {
|
|
99190
99394
|
const extra = /* @__PURE__ */ new Map();
|
|
99191
99395
|
const edgeEnds = new Map(model.flows.map((flow) => [flow.id, /* @__PURE__ */ new Set([flow.from, flow.to])]));
|
|
@@ -99243,6 +99447,27 @@ function relayoutVerdict(before, after) {
|
|
|
99243
99447
|
}
|
|
99244
99448
|
return -1;
|
|
99245
99449
|
}
|
|
99450
|
+
function sideToward(from, to) {
|
|
99451
|
+
const dx = centerOf(to).x - centerOf(from).x;
|
|
99452
|
+
const dy = centerOf(to).y - centerOf(from).y;
|
|
99453
|
+
return Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? "WEST" : "EAST" : dy < 0 ? "NORTH" : "SOUTH";
|
|
99454
|
+
}
|
|
99455
|
+
function sideOn(p, n) {
|
|
99456
|
+
if (p.x > n.x - 2 && p.x < n.x + n.width + 2) {
|
|
99457
|
+
if (Math.abs(p.y - n.y) < 2) return "NORTH";
|
|
99458
|
+
if (Math.abs(p.y - (n.y + n.height)) < 2) return "SOUTH";
|
|
99459
|
+
}
|
|
99460
|
+
if (p.y > n.y - 2 && p.y < n.y + n.height + 2) {
|
|
99461
|
+
if (Math.abs(p.x - n.x) < 2) return "WEST";
|
|
99462
|
+
if (Math.abs(p.x - (n.x + n.width)) < 2) return "EAST";
|
|
99463
|
+
}
|
|
99464
|
+
return null;
|
|
99465
|
+
}
|
|
99466
|
+
function firstPassSide(scene, flowId, role, sceneNode) {
|
|
99467
|
+
const edge = scene.edges.find((candidate) => candidate.id === flowId);
|
|
99468
|
+
if (!edge || edge.pts.length < 2) return null;
|
|
99469
|
+
return sideOn(role === "src" ? edge.pts[0] : edge.pts[edge.pts.length - 1], sceneNode);
|
|
99470
|
+
}
|
|
99246
99471
|
function constrainPorts(graph, scene, flagged, model) {
|
|
99247
99472
|
const nodeById = new Map(scene.nodes.map((node) => [node.id, node]));
|
|
99248
99473
|
const elkById = /* @__PURE__ */ new Map();
|
|
@@ -99251,23 +99476,6 @@ function constrainPorts(graph, scene, flagged, model) {
|
|
|
99251
99476
|
for (const child of node.children ?? []) register(child);
|
|
99252
99477
|
};
|
|
99253
99478
|
register(graph);
|
|
99254
|
-
const centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
|
|
99255
|
-
const sideToward = (from, to) => {
|
|
99256
|
-
const dx = centerOf(to).x - centerOf(from).x;
|
|
99257
|
-
const dy = centerOf(to).y - centerOf(from).y;
|
|
99258
|
-
return Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? "WEST" : "EAST" : dy < 0 ? "NORTH" : "SOUTH";
|
|
99259
|
-
};
|
|
99260
|
-
const sideOn = (p, n) => {
|
|
99261
|
-
if (p.x > n.x - 2 && p.x < n.x + n.width + 2) {
|
|
99262
|
-
if (Math.abs(p.y - n.y) < 2) return "NORTH";
|
|
99263
|
-
if (Math.abs(p.y - (n.y + n.height)) < 2) return "SOUTH";
|
|
99264
|
-
}
|
|
99265
|
-
if (p.y > n.y - 2 && p.y < n.y + n.height + 2) {
|
|
99266
|
-
if (Math.abs(p.x - n.x) < 2) return "WEST";
|
|
99267
|
-
if (Math.abs(p.x - (n.x + n.width)) < 2) return "EAST";
|
|
99268
|
-
}
|
|
99269
|
-
return null;
|
|
99270
|
-
};
|
|
99271
99479
|
const flaggedNodes = /* @__PURE__ */ new Set();
|
|
99272
99480
|
for (const flow of model.flows)
|
|
99273
99481
|
if (flagged.has(flow.id)) {
|
|
@@ -99284,14 +99492,9 @@ function constrainPorts(graph, scene, flagged, model) {
|
|
|
99284
99492
|
const role = flow.from === nodeId ? "src" : "dst";
|
|
99285
99493
|
const other = nodeById.get(role === "src" ? flow.to : flow.from);
|
|
99286
99494
|
if (!other) continue;
|
|
99287
|
-
|
|
99288
|
-
if (!flagged.has(flow.id)) {
|
|
99289
|
-
const edge = scene.edges.find((e) => e.id === flow.id);
|
|
99290
|
-
const terminal = edge && edge.pts.length >= 2 ? role === "src" ? edge.pts[0] : edge.pts[edge.pts.length - 1] : null;
|
|
99291
|
-
const actual = terminal ? sideOn(terminal, sceneNode) : null;
|
|
99292
|
-
if (actual) side = actual;
|
|
99293
|
-
}
|
|
99495
|
+
const side = flagged.has(flow.id) ? sideToward(sceneNode, other) : firstPassSide(scene, flow.id, role, sceneNode) ?? sideToward(sceneNode, other);
|
|
99294
99496
|
const portId = `${flow.id}${role === "src" ? "#out" : "#in"}`;
|
|
99497
|
+
if ((elkNode.ports ?? []).some((port) => port.id === portId)) continue;
|
|
99295
99498
|
ports.push({ id: portId, width: 1, height: 1, layoutOptions: { "elk.port.side": side } });
|
|
99296
99499
|
const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
|
|
99297
99500
|
if (elkEdge) {
|
|
@@ -99312,6 +99515,52 @@ function elkPartitionOf(element, index, view, ingressExternal) {
|
|
|
99312
99515
|
if (view.partitions[element.kind] !== void 0) return 90 + view.partitions[element.kind];
|
|
99313
99516
|
return index;
|
|
99314
99517
|
}
|
|
99518
|
+
function rootAncestorOf(model, id) {
|
|
99519
|
+
let element = model.index.get(id);
|
|
99520
|
+
if (!element) return void 0;
|
|
99521
|
+
while (element.parent) element = element.parent;
|
|
99522
|
+
return element.id;
|
|
99523
|
+
}
|
|
99524
|
+
function readingSlots(model, view, ingressExternal) {
|
|
99525
|
+
const slotOf = /* @__PURE__ */ new Map();
|
|
99526
|
+
if (!model.elements.some((element) => element.order)) return slotOf;
|
|
99527
|
+
const bandOf = new Map(
|
|
99528
|
+
model.elements.map((element, index) => [
|
|
99529
|
+
element.id,
|
|
99530
|
+
elkPartitionOf(element, index, view, ingressExternal)
|
|
99531
|
+
])
|
|
99532
|
+
);
|
|
99533
|
+
const lowestOf = /* @__PURE__ */ new Map();
|
|
99534
|
+
for (const band of new Set(bandOf.values())) {
|
|
99535
|
+
const members = model.elements.filter((element) => bandOf.get(element.id) === band);
|
|
99536
|
+
const declared = [
|
|
99537
|
+
...new Set(members.filter((m) => m.order).map((m) => m.order.value))
|
|
99538
|
+
].sort((a, b) => a - b);
|
|
99539
|
+
const lowest = declared.length ? 1 : 0;
|
|
99540
|
+
lowestOf.set(band, lowest);
|
|
99541
|
+
for (const member of members)
|
|
99542
|
+
slotOf.set(
|
|
99543
|
+
member.id,
|
|
99544
|
+
member.order ? declared.indexOf(member.order.value) + 1 : lowest
|
|
99545
|
+
);
|
|
99546
|
+
}
|
|
99547
|
+
const ordered = new Set(model.elements.filter((element) => element.order).map((e) => e.id));
|
|
99548
|
+
for (let pass = 0; pass < model.elements.length; pass++) {
|
|
99549
|
+
let moved = false;
|
|
99550
|
+
for (const flow of model.flows) {
|
|
99551
|
+
const from = rootAncestorOf(model, flow.from);
|
|
99552
|
+
const to = rootAncestorOf(model, flow.to);
|
|
99553
|
+
if (!from || !to || from === to || ordered.has(to)) continue;
|
|
99554
|
+
if (bandOf.get(from) !== bandOf.get(to)) continue;
|
|
99555
|
+
const candidate = slotOf.get(from);
|
|
99556
|
+
if (candidate <= slotOf.get(to)) continue;
|
|
99557
|
+
slotOf.set(to, candidate);
|
|
99558
|
+
moved = true;
|
|
99559
|
+
}
|
|
99560
|
+
if (!moved) break;
|
|
99561
|
+
}
|
|
99562
|
+
return slotOf;
|
|
99563
|
+
}
|
|
99315
99564
|
function elkFlowEdge(flow, ctx, labelWrap) {
|
|
99316
99565
|
const { compact, numbered, fonts, businessObjectName } = ctx;
|
|
99317
99566
|
if (numbered)
|
|
@@ -99348,9 +99597,82 @@ function elkFlowEdge(flow, ctx, labelWrap) {
|
|
|
99348
99597
|
labels: text || chips.length ? [{ text, ...labelBox }] : []
|
|
99349
99598
|
};
|
|
99350
99599
|
}
|
|
99600
|
+
function applyDeclaredPorts(graph, model) {
|
|
99601
|
+
const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide);
|
|
99602
|
+
if (!pinned.length) return;
|
|
99603
|
+
const elkById = /* @__PURE__ */ new Map();
|
|
99604
|
+
const register = (node) => {
|
|
99605
|
+
elkById.set(node.id, node);
|
|
99606
|
+
for (const child of node.children ?? []) register(child);
|
|
99607
|
+
};
|
|
99608
|
+
register(graph);
|
|
99609
|
+
for (const flow of pinned) {
|
|
99610
|
+
const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
|
|
99611
|
+
if (!elkEdge) continue;
|
|
99612
|
+
for (const [role, declared, nodeId] of [
|
|
99613
|
+
["out", flow.fromSide, flow.from],
|
|
99614
|
+
["in", flow.toSide, flow.to]
|
|
99615
|
+
]) {
|
|
99616
|
+
if (!declared) continue;
|
|
99617
|
+
const elkNode = elkById.get(nodeId);
|
|
99618
|
+
if (!elkNode) continue;
|
|
99619
|
+
const portId = `${flow.id}#${role}`;
|
|
99620
|
+
elkNode.ports = [
|
|
99621
|
+
...elkNode.ports ?? [],
|
|
99622
|
+
{
|
|
99623
|
+
id: portId,
|
|
99624
|
+
width: 1,
|
|
99625
|
+
height: 1,
|
|
99626
|
+
layoutOptions: { "elk.port.side": SIDE_TO_ELK[declared.value] }
|
|
99627
|
+
}
|
|
99628
|
+
];
|
|
99629
|
+
elkNode.layoutOptions = { ...elkNode.layoutOptions, "elk.portConstraints": "FIXED_SIDE" };
|
|
99630
|
+
if (role === "out") elkEdge.sources = [portId];
|
|
99631
|
+
else elkEdge.targets = [portId];
|
|
99632
|
+
}
|
|
99633
|
+
}
|
|
99634
|
+
}
|
|
99635
|
+
function terminalSide(p, node) {
|
|
99636
|
+
if (p.x > node.x - 2 && p.x < node.x + node.width + 2) {
|
|
99637
|
+
if (Math.abs(p.y - node.y) < 2) return "top";
|
|
99638
|
+
if (Math.abs(p.y - (node.y + node.height)) < 2) return "bottom";
|
|
99639
|
+
}
|
|
99640
|
+
if (p.y > node.y - 2 && p.y < node.y + node.height + 2) {
|
|
99641
|
+
if (Math.abs(p.x - node.x) < 2) return "left";
|
|
99642
|
+
if (Math.abs(p.x - (node.x + node.width)) < 2) return "right";
|
|
99643
|
+
}
|
|
99644
|
+
return null;
|
|
99645
|
+
}
|
|
99646
|
+
function attachSideDiagnostics(scene, model) {
|
|
99647
|
+
const diagnostics = [];
|
|
99648
|
+
const nodeById = new Map(scene.nodes.map((node) => [node.id, node]));
|
|
99649
|
+
for (const flow of model.flows) {
|
|
99650
|
+
if (!flow.fromSide && !flow.toSide) continue;
|
|
99651
|
+
const edge = scene.edges.find((candidate) => candidate.id === flow.id);
|
|
99652
|
+
if (!edge || edge.pts.length < 2) continue;
|
|
99653
|
+
for (const [declared, nodeId, point, role] of [
|
|
99654
|
+
[flow.fromSide, flow.from, edge.pts[0], "leaves"],
|
|
99655
|
+
[flow.toSide, flow.to, edge.pts[edge.pts.length - 1], "arrives on"]
|
|
99656
|
+
]) {
|
|
99657
|
+
const node = declared ? nodeById.get(nodeId) : void 0;
|
|
99658
|
+
if (!declared || !node) continue;
|
|
99659
|
+
const actual = terminalSide(point, node);
|
|
99660
|
+
if (actual === declared.value) continue;
|
|
99661
|
+
diagnostics.push({
|
|
99662
|
+
code: "W0570",
|
|
99663
|
+
severity: "warning",
|
|
99664
|
+
message: `attachment side \`${declared.value}\` could not be honored`,
|
|
99665
|
+
span: declared.span,
|
|
99666
|
+
note: actual ? `the flow ${role} the ${actual} side of \`${nodeId}\`` : `the flow does not ${role} a side of \`${nodeId}\` cleanly`,
|
|
99667
|
+
help: "a side the layout cannot reach is dropped rather than forced \u2014 try the opposite endpoint, or `order:` to move the element instead"
|
|
99668
|
+
});
|
|
99669
|
+
}
|
|
99670
|
+
}
|
|
99671
|
+
return diagnostics;
|
|
99672
|
+
}
|
|
99351
99673
|
function buildElkGraph(ctx, direction, options) {
|
|
99352
|
-
const { model, view, ingressExternal, compact, numbered, fonts } = ctx;
|
|
99353
|
-
|
|
99674
|
+
const { model, view, ingressExternal, slotOf, compact, numbered, fonts } = ctx;
|
|
99675
|
+
const graph = {
|
|
99354
99676
|
id: "root",
|
|
99355
99677
|
layoutOptions: {
|
|
99356
99678
|
"elk.algorithm": "layered",
|
|
@@ -99388,17 +99710,23 @@ function buildElkGraph(ctx, direction, options) {
|
|
|
99388
99710
|
} : {}
|
|
99389
99711
|
},
|
|
99390
99712
|
children: model.elements.map((element, index) => {
|
|
99391
|
-
const elkNode = toElkNode2(element, compact, fonts
|
|
99713
|
+
const elkNode = toElkNode2(element, compact, fonts, true);
|
|
99714
|
+
const band = elkPartitionOf(element, index, view, ingressExternal);
|
|
99715
|
+
const slot = slotOf.get(element.id);
|
|
99392
99716
|
elkNode.layoutOptions = {
|
|
99393
99717
|
...elkNode.layoutOptions,
|
|
99718
|
+
// Without an `order:` anywhere the band is emitted as it always was, so
|
|
99719
|
+
// the drawing is byte-identical to one from before the hint existed.
|
|
99394
99720
|
"elk.partitioning.partition": String(
|
|
99395
|
-
|
|
99721
|
+
slot === void 0 ? band : band * SLOT_SCALE + slot
|
|
99396
99722
|
)
|
|
99397
99723
|
};
|
|
99398
99724
|
return elkNode;
|
|
99399
99725
|
}),
|
|
99400
99726
|
edges: model.flows.map((flow) => elkFlowEdge(flow, ctx, options?.labelWrap))
|
|
99401
99727
|
};
|
|
99728
|
+
applyDeclaredPorts(graph, model);
|
|
99729
|
+
return graph;
|
|
99402
99730
|
}
|
|
99403
99731
|
function recordRepairs(scene, routesBefore) {
|
|
99404
99732
|
for (const edge of scene.edges) {
|
|
@@ -99453,6 +99781,7 @@ async function layout(model, view) {
|
|
|
99453
99781
|
model,
|
|
99454
99782
|
view,
|
|
99455
99783
|
ingressExternal: ingressExternalElements,
|
|
99784
|
+
slotOf: readingSlots(model, view, ingressExternalElements),
|
|
99456
99785
|
compact,
|
|
99457
99786
|
numbered,
|
|
99458
99787
|
fonts: {
|
|
@@ -99473,18 +99802,25 @@ async function layout(model, view) {
|
|
|
99473
99802
|
const nodes = walkedNodes.map((walked) => walked.node);
|
|
99474
99803
|
for (const walked of walkedNodes) origins[walked.id] = { x: walked.x, y: walked.y };
|
|
99475
99804
|
const edges = collectSceneEdges(result2, origins, numbered, edgeFontSize);
|
|
99476
|
-
const
|
|
99805
|
+
const pinnedFlows = new Map(
|
|
99806
|
+
model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
|
|
99807
|
+
);
|
|
99808
|
+
for (const edge of edges) {
|
|
99809
|
+
const pinned = pinnedFlows.get(edge.id);
|
|
99810
|
+
if (pinned) edge.pinned = { ...pinned };
|
|
99811
|
+
}
|
|
99812
|
+
const scene = {
|
|
99477
99813
|
width: Math.ceil(result2.width),
|
|
99478
99814
|
height: Math.ceil(result2.height),
|
|
99479
99815
|
nodes,
|
|
99480
99816
|
edges,
|
|
99481
99817
|
layoutMs: layoutMs2
|
|
99482
99818
|
};
|
|
99483
|
-
runGeometryPasses(
|
|
99819
|
+
runGeometryPasses(scene, model, {
|
|
99484
99820
|
numbered,
|
|
99485
99821
|
sideways: disposition === "page" || disposition === "tall"
|
|
99486
99822
|
});
|
|
99487
|
-
return
|
|
99823
|
+
return scene;
|
|
99488
99824
|
};
|
|
99489
99825
|
const startTime = Date.now();
|
|
99490
99826
|
let result;
|
|
@@ -99530,30 +99866,39 @@ async function layout(model, view) {
|
|
|
99530
99866
|
result = await elk.layout(makeGraph(winnerDirection));
|
|
99531
99867
|
}
|
|
99532
99868
|
const layoutMs = Date.now() - startTime;
|
|
99533
|
-
const
|
|
99534
|
-
const away = attachAwayOf(scene, model);
|
|
99869
|
+
const base = sceneFromResult(result, layoutMs);
|
|
99535
99870
|
const skipPortPass = !!globalThis.process?.env?.CAIRN_NO_PORT_PASS;
|
|
99536
|
-
if (
|
|
99537
|
-
|
|
99538
|
-
const
|
|
99539
|
-
|
|
99540
|
-
const
|
|
99541
|
-
|
|
99542
|
-
|
|
99543
|
-
|
|
99544
|
-
|
|
99545
|
-
|
|
99546
|
-
|
|
99547
|
-
);
|
|
99548
|
-
|
|
99549
|
-
|
|
99550
|
-
|
|
99551
|
-
|
|
99552
|
-
|
|
99553
|
-
|
|
99871
|
+
if (skipPortPass) return base;
|
|
99872
|
+
const layoutProfile = (candidate) => {
|
|
99873
|
+
const everyEdge = new Set(candidate.edges.map((edge) => edge.id));
|
|
99874
|
+
const profile = inspect(candidate, titleBoxesOf(candidate, model)).local(everyEdge, /* @__PURE__ */ new Map());
|
|
99875
|
+
for (const [key, tier] of selectionExtras(candidate, model)) profile.set(key, tier);
|
|
99876
|
+
return profile;
|
|
99877
|
+
};
|
|
99878
|
+
const baseProfile = layoutProfile(base);
|
|
99879
|
+
let current = base;
|
|
99880
|
+
let best = null;
|
|
99881
|
+
for (let round = 0; round < PORT_PASS_ROUNDS; round++) {
|
|
99882
|
+
const flagged = attachAwayOf(current, model);
|
|
99883
|
+
if (!flagged.size) break;
|
|
99884
|
+
let candidate;
|
|
99885
|
+
try {
|
|
99886
|
+
const constrained = makeGraph(winnerDirection, winnerOptions);
|
|
99887
|
+
constrainPorts(constrained, current, flagged, model);
|
|
99888
|
+
const reresult = await elk.layout(constrained);
|
|
99889
|
+
candidate = sceneFromResult(reresult, Date.now() - startTime);
|
|
99890
|
+
} catch {
|
|
99891
|
+
break;
|
|
99892
|
+
}
|
|
99893
|
+
const tier = relayoutVerdict(baseProfile, layoutProfile(candidate));
|
|
99894
|
+
if (tier < 0) break;
|
|
99895
|
+
const scored = { scene: candidate, tier, detours: longDetourCount(candidate, model) };
|
|
99896
|
+
if (beatsRelayout(scored, best)) best = scored;
|
|
99897
|
+
current = candidate;
|
|
99554
99898
|
}
|
|
99899
|
+
return best?.scene ?? base;
|
|
99555
99900
|
}
|
|
99556
|
-
var INGRESS_PARTITION, EGRESS_PARTITION, COMPACT_WRAP;
|
|
99901
|
+
var orderOption, semiInteractiveOption, PORT_PASS_ROUNDS, centerOf, INGRESS_PARTITION, EGRESS_PARTITION, COMPACT_WRAP, SLOT_SCALE, SIDE_TO_ELK;
|
|
99557
99902
|
var init_scene_layout = __esm({
|
|
99558
99903
|
"src/scene-layout.ts"() {
|
|
99559
99904
|
"use strict";
|
|
@@ -99561,14 +99906,26 @@ var init_scene_layout = __esm({
|
|
|
99561
99906
|
init_slide_fold();
|
|
99562
99907
|
init_elk_engine();
|
|
99563
99908
|
init_route_detour();
|
|
99909
|
+
init_geometry();
|
|
99564
99910
|
init_compact();
|
|
99565
99911
|
init_edge_tidy();
|
|
99566
99912
|
init_readability();
|
|
99567
99913
|
init_label_anchor();
|
|
99568
99914
|
init_element_tree();
|
|
99915
|
+
orderOption = (element) => element.order ? { "elk.position": `(0,${element.order.value})` } : {};
|
|
99916
|
+
semiInteractiveOption = (children) => children.some((child) => child.order) ? { "elk.layered.crossingMinimization.semiInteractive": "true" } : {};
|
|
99917
|
+
PORT_PASS_ROUNDS = 2;
|
|
99918
|
+
centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
|
|
99569
99919
|
INGRESS_PARTITION = -1;
|
|
99570
99920
|
EGRESS_PARTITION = 900;
|
|
99571
99921
|
COMPACT_WRAP = 10;
|
|
99922
|
+
SLOT_SCALE = 1e3;
|
|
99923
|
+
SIDE_TO_ELK = {
|
|
99924
|
+
left: "WEST",
|
|
99925
|
+
right: "EAST",
|
|
99926
|
+
top: "NORTH",
|
|
99927
|
+
bottom: "SOUTH"
|
|
99928
|
+
};
|
|
99572
99929
|
}
|
|
99573
99930
|
});
|
|
99574
99931
|
|
|
@@ -100224,7 +100581,7 @@ function createEdgePainter(paint) {
|
|
|
100224
100581
|
const flowStyle = flow?.style;
|
|
100225
100582
|
const color = flowColorOf(flow);
|
|
100226
100583
|
const headColor = style.flowColor === "by-source" ? color : defaultEdgeColor;
|
|
100227
|
-
const dash = dashArray(flowStyle?.stroke?.style ?? style.flowStroke.style);
|
|
100584
|
+
const dash = dashArray(flowStyle?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style);
|
|
100228
100585
|
const width = flowStyle?.stroke?.width ?? style.flowStroke.width;
|
|
100229
100586
|
return `<path d="${edgePath(edge.pts)}" fill="none" stroke="${escAttr(color)}" stroke-width="${width}"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(headColor)})"/>
|
|
100230
100587
|
`;
|
|
@@ -100642,6 +100999,7 @@ function watchCommand(file, outFile) {
|
|
|
100642
100999
|
try {
|
|
100643
101000
|
const view = views[model.type];
|
|
100644
101001
|
const scene = await layout(model, view);
|
|
101002
|
+
diags.push(...attachSideDiagnostics(scene, model));
|
|
100645
101003
|
const { svg, overlapsAfter } = render(model, view, scene);
|
|
100646
101004
|
writeFileSync(outFile, svg);
|
|
100647
101005
|
if (diags.length)
|
|
@@ -100938,7 +101296,7 @@ APP -> DB : "Read/write" (TLS1.3)
|
|
|
100938
101296
|
};
|
|
100939
101297
|
args = process.argv.slice(2);
|
|
100940
101298
|
command = args[0];
|
|
100941
|
-
version = true ? "1.0.0-
|
|
101299
|
+
version = true ? "1.0.0-RC13" : pkg.version;
|
|
100942
101300
|
VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--format"]);
|
|
100943
101301
|
positionalFile = () => {
|
|
100944
101302
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -100987,6 +101345,7 @@ APP -> DB : "Read/write" (TLS1.3)
|
|
|
100987
101345
|
model.typeSpan ?? { line: 1, col: 1, len: 7 }
|
|
100988
101346
|
);
|
|
100989
101347
|
if (diagnostic) diagnostics.push(diagnostic);
|
|
101348
|
+
diagnostics.push(...attachSideDiagnostics(scene, model));
|
|
100990
101349
|
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
100991
101350
|
if (warnings.length)
|
|
100992
101351
|
console.error(renderHuman(file, src, warnings, process.stderr.isTTY ?? false));
|