@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/dist/cairn.mjs CHANGED
@@ -92256,10 +92256,13 @@ function lex(src, diagnostics) {
92256
92256
  col++;
92257
92257
  continue;
92258
92258
  }
92259
- if (char === "-" && src[position + 1] === ">") {
92260
- push("arrow", "->", line, col);
92261
- position += 2;
92262
- col += 2;
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];
@@ -92299,6 +92302,7 @@ function lex(src, diagnostics) {
92299
92302
  }
92300
92303
 
92301
92304
  // src/models/ast.ts
92305
+ var ATTACH_SIDES = ["left", "right", "top", "bottom"];
92302
92306
  var defaultDiagramStyle = () => ({
92303
92307
  crossingHops: true,
92304
92308
  compact: false,
@@ -92935,9 +92939,9 @@ function indexElementsById(elements) {
92935
92939
  // src/parser.ts
92936
92940
  function parseFlow(p, sourceToken) {
92937
92941
  const { matchToken, advance, reportError, lookAhead, syncToNextLine, model } = p;
92938
- advance();
92942
+ const arrowToken = advance();
92939
92943
  if (!matchToken("id")) {
92940
- reportError("target identifier expected after `->`", lookAhead().span);
92944
+ reportError(`target identifier expected after \`${arrowToken.text}\``, lookAhead().span);
92941
92945
  syncToNextLine();
92942
92946
  return;
92943
92947
  }
@@ -92954,6 +92958,8 @@ function parseFlow(p, sourceToken) {
92954
92958
  len: targetToken.span.col + targetToken.span.len - sourceToken.span.col
92955
92959
  }
92956
92960
  };
92961
+ if (arrowToken.text === "-->") flow.lineStyle = "dashed";
92962
+ else if (arrowToken.text === "..>") flow.lineStyle = "dotted";
92957
92963
  if (matchToken("colon")) {
92958
92964
  advance();
92959
92965
  if (matchToken("str")) flow.label = advance().text;
@@ -93006,6 +93012,11 @@ function parseFlow(p, sourceToken) {
93006
93012
  flow.style = {};
93007
93013
  p.parseStyleEntries(null, flow.style);
93008
93014
  }
93015
+ for (const split of [
93016
+ splitEndpoint(sourceToken, flow, "from"),
93017
+ splitEndpoint(targetToken, flow, "to")
93018
+ ])
93019
+ if (split) p.endpointSplits.push(split);
93009
93020
  model.flows.push(flow);
93010
93021
  }
93011
93022
  function parseElement(p, sourceToken, parent) {
@@ -93057,6 +93068,84 @@ function parseElement(p, sourceToken, parent) {
93057
93068
  }
93058
93069
  (parent ? parent.children : model.elements).push(element);
93059
93070
  }
93071
+ function splitEndpoint(token, flow, role) {
93072
+ const dot = token.text.lastIndexOf(".");
93073
+ if (dot <= 0 || dot === token.text.length - 1) return null;
93074
+ const base = token.text.slice(0, dot);
93075
+ const side = token.text.slice(dot + 1);
93076
+ return {
93077
+ flow,
93078
+ role,
93079
+ raw: token.text,
93080
+ rawSpan: token.span,
93081
+ base,
93082
+ baseSpan: { line: token.span.line, col: token.span.col, len: base.length },
93083
+ side,
93084
+ sideSpan: { line: token.span.line, col: token.span.col + dot + 1, len: side.length }
93085
+ };
93086
+ }
93087
+ function resolveAttachSides(splits, model, diagnostics) {
93088
+ for (const split of splits) {
93089
+ if (model.index.has(split.raw)) {
93090
+ if (ATTACH_SIDES.includes(split.side))
93091
+ diagnostics.push({
93092
+ code: "W0571",
93093
+ severity: "warning",
93094
+ message: `\`${split.raw}\` is a declared element, so \`.${split.side}\` is not read as an attachment side`,
93095
+ span: split.rawSpan,
93096
+ note: "a declared id always wins over the `ID.side` reading",
93097
+ help: `rename the element if you meant to attach the flow to the ${split.side} side of \`${split.base}\``
93098
+ });
93099
+ continue;
93100
+ }
93101
+ if (!model.index.has(split.base)) continue;
93102
+ const known = ATTACH_SIDES.includes(split.side);
93103
+ if (!known)
93104
+ diagnostics.push({
93105
+ code: "E0223",
93106
+ severity: "error",
93107
+ message: `unknown attachment side \`${split.side}\``,
93108
+ span: split.sideSpan,
93109
+ note: "sides are named as the diagram is read",
93110
+ help: "use `left`, `right`, `top` or `bottom`, e.g. `APP.right -> DB.left`"
93111
+ });
93112
+ const side = known ? { value: split.side, span: split.sideSpan } : void 0;
93113
+ if (split.role === "from") {
93114
+ split.flow.from = split.base;
93115
+ split.flow.fromSpan = split.baseSpan;
93116
+ split.flow.fromSide = side;
93117
+ } else {
93118
+ split.flow.to = split.base;
93119
+ split.flow.toSpan = split.baseSpan;
93120
+ split.flow.toSide = side;
93121
+ }
93122
+ }
93123
+ }
93124
+ function tryOrderEntry(p, parent) {
93125
+ const { matchToken, advance, reportCoded, save, restore, syncToNextLine } = p;
93126
+ if (!parent || !matchToken("id", "order")) return false;
93127
+ const mark = save();
93128
+ const keyToken = advance();
93129
+ if (!matchToken("colon")) {
93130
+ restore(mark);
93131
+ return false;
93132
+ }
93133
+ advance();
93134
+ const valueToken = matchToken("num") ? advance() : null;
93135
+ const value = valueToken ? Number(valueToken.text) : Number.NaN;
93136
+ if (!Number.isInteger(value) || value < 0) {
93137
+ reportCoded(
93138
+ "E0106",
93139
+ "`order` expects a whole number \u2265 0",
93140
+ (valueToken ?? keyToken).span,
93141
+ "e.g. `order: 1` \u2014 lower comes first in reading order"
93142
+ );
93143
+ syncToNextLine();
93144
+ return true;
93145
+ }
93146
+ parent.order = { value, span: valueToken.span };
93147
+ return true;
93148
+ }
93060
93149
  function tryLegendBlock(p) {
93061
93150
  const { matchToken, advance, reportError, lookAhead, skipNewlines, syncToNextLine, model } = p;
93062
93151
  if (!matchToken("id", "legend")) return false;
@@ -93134,13 +93223,14 @@ function parse(src) {
93134
93223
  const skipNewlines = () => {
93135
93224
  while (matchToken("nl")) advance();
93136
93225
  };
93137
- const reportError = (message, span, help) => diagnostics.push({
93138
- code: "E0101",
93226
+ const reportCoded = (code, message, span, help) => diagnostics.push({
93227
+ code,
93139
93228
  severity: "error",
93140
93229
  message,
93141
93230
  span,
93142
93231
  help
93143
93232
  });
93233
+ const reportError = (message, span, help) => reportCoded("E0101", message, span, help);
93144
93234
  const syncToNextLine = () => {
93145
93235
  while (!matchToken("nl") && !matchToken("eof")) advance();
93146
93236
  };
@@ -93153,6 +93243,7 @@ function parse(src) {
93153
93243
  index: /* @__PURE__ */ new Map()
93154
93244
  };
93155
93245
  const flowSequence = { next: 0 };
93246
+ const endpointSplits = [];
93156
93247
  skipNewlines();
93157
93248
  if (matchToken("id", "diagram")) {
93158
93249
  advance();
@@ -93244,12 +93335,14 @@ function parse(src) {
93244
93335
  advance,
93245
93336
  skipNewlines,
93246
93337
  reportError,
93338
+ reportCoded,
93247
93339
  syncToNextLine,
93248
93340
  save: () => position,
93249
93341
  restore: (mark) => {
93250
93342
  position = mark;
93251
93343
  },
93252
93344
  flowSequence,
93345
+ endpointSplits,
93253
93346
  parseStyleEntries,
93254
93347
  parseElementBody
93255
93348
  };
@@ -93259,6 +93352,7 @@ function parse(src) {
93259
93352
  return;
93260
93353
  }
93261
93354
  if (tryStyleBlock(parent)) return;
93355
+ if (tryOrderEntry(parser, parent)) return;
93262
93356
  if (!parent && tryLegendBlock(parser)) return;
93263
93357
  if (!parent && tryBusinessObject(parser)) return;
93264
93358
  parseFlowOrElement(parser, parent);
@@ -93271,6 +93365,7 @@ function parse(src) {
93271
93365
  for (const [id, element] of indexElementsById(model.elements)) {
93272
93366
  if (!model.index.has(id)) model.index.set(id, element);
93273
93367
  }
93368
+ resolveAttachSides(endpointSplits, model, diagnostics);
93274
93369
  return { model, diags: diagnostics };
93275
93370
  }
93276
93371
  var LINE_STYLES = /* @__PURE__ */ new Set(["solid", "dashed", "dotted"]);
@@ -93616,12 +93711,27 @@ var logicalView = {
93616
93711
  };
93617
93712
  var applicationView = {
93618
93713
  name: "application",
93619
- kinds: ["actor-group", "actor", "application", "module", "queue", "datastore", "external"],
93620
- containerKinds: ["actor-group", "application", "external"],
93714
+ kinds: [
93715
+ "actor-group",
93716
+ "actor",
93717
+ "system",
93718
+ "application",
93719
+ "module",
93720
+ "queue",
93721
+ "datastore",
93722
+ "external"
93723
+ ],
93724
+ containerKinds: ["actor-group", "system", "application", "external"],
93621
93725
  // C4-style `(API_REST, JSON)`: the protocol half is worth tabulating, the port is not.
93622
- matrix: { zoneKinds: ["application"], columns: ["num", "source", "dest", "proto", "nature"] },
93726
+ // `zoneOf` walks ancestors nearest-first, so an endpoint inside an application
93727
+ // still reads `Name (App)`; the system only annotates what sits directly in it.
93728
+ matrix: {
93729
+ zoneKinds: ["application", "system"],
93730
+ columns: ["num", "source", "dest", "proto", "nature"]
93731
+ },
93623
93732
  partitions: {
93624
93733
  "actor-group": 0,
93734
+ system: 1,
93625
93735
  application: 1,
93626
93736
  queue: 1,
93627
93737
  datastore: 1,
@@ -93630,6 +93740,7 @@ var applicationView = {
93630
93740
  legendNames: {
93631
93741
  "actor-group": "Actor group",
93632
93742
  actor: "Actor",
93743
+ system: "System",
93633
93744
  application: "Application",
93634
93745
  module: "Application module",
93635
93746
  queue: "Message queue / broker",
@@ -93639,6 +93750,7 @@ var applicationView = {
93639
93750
  legendNamesFr: {
93640
93751
  "actor-group": "Groupe d'acteurs",
93641
93752
  actor: "Acteur",
93753
+ system: "Syst\xE8me",
93642
93754
  application: "Application",
93643
93755
  module: "Module applicatif",
93644
93756
  queue: "File de messages / broker",
@@ -93686,6 +93798,12 @@ var applicationView = {
93686
93798
  fill: "#eef4fb",
93687
93799
  stroke: { color: "#7a9cc4", style: "dashed", width: 1.2 }
93688
93800
  },
93801
+ // Same palette as the logical view's `system`, so a system boundary reads
93802
+ // the same in both views.
93803
+ system: {
93804
+ fill: "#f6f2ea",
93805
+ stroke: { color: "#b09a6d", style: "solid", width: 1.2 }
93806
+ },
93689
93807
  application: {
93690
93808
  fill: "#e8f1f8",
93691
93809
  stroke: { color: "#5b8db8", style: "solid", width: 1.2 }
@@ -93713,6 +93831,10 @@ var applicationView = {
93713
93831
  fill: "#232a33",
93714
93832
  stroke: { color: "#5c7fa8", style: "dashed", width: 1.2 }
93715
93833
  },
93834
+ system: {
93835
+ fill: "#2b2822",
93836
+ stroke: { color: "#9c8558", style: "solid", width: 1.2 }
93837
+ },
93716
93838
  application: {
93717
93839
  fill: "#1f2a33",
93718
93840
  stroke: { color: "#4a7ba6", style: "solid", width: 1.2 }
@@ -95108,6 +95230,14 @@ var pathLength = (points) => {
95108
95230
  length += Math.abs(points[index + 1].x - points[index].x) + Math.abs(points[index + 1].y - points[index].y);
95109
95231
  return length;
95110
95232
  };
95233
+ var DETOUR_RATIO = 2.2;
95234
+ var DETOUR_WASTE = 400;
95235
+ var isLongDetour = (points, boxA, boxB) => {
95236
+ 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));
95237
+ if (direct <= 0) return false;
95238
+ const length = pathLength(points);
95239
+ return length > DETOUR_RATIO * direct && length - direct > DETOUR_WASTE;
95240
+ };
95111
95241
  var boxToPolylineSq = (box, points) => {
95112
95242
  let best = Number.POSITIVE_INFINITY;
95113
95243
  for (let index = 0; index + 1 < points.length; index++) {
@@ -95173,6 +95303,7 @@ function detourCandidates(scene, model) {
95173
95303
  for (const edge of scene.edges) {
95174
95304
  const flow = flowById.get(edge.id);
95175
95305
  if (!flow || edge.pts.length < 2) continue;
95306
+ if (edge.pinned) continue;
95176
95307
  const source = nodeById.get(flow.from);
95177
95308
  const target = nodeById.get(flow.to);
95178
95309
  if (!source || !target) continue;
@@ -95196,7 +95327,9 @@ function verticalConflict(segment, x, top, bottom) {
95196
95327
  var blockedBelowBy = (leafBoxes, x, top) => leafBoxes.some(
95197
95328
  (node) => x >= node.x - 2 && x <= node.x + node.width + 2 && node.y + node.height > top + 1
95198
95329
  );
95199
- var blockedAboveBy = (leafBoxes, titleBoxes, x, bottom) => leafBoxes.some((node) => x >= node.x - 2 && x <= node.x + node.width + 2 && node.y < bottom - 1) || titleBoxes.some(
95330
+ var blockedAboveBy = (leafBoxes, titleBoxes, x, bottom) => leafBoxes.some(
95331
+ (node) => x >= node.x - 2 && x <= node.x + node.width + 2 && node.y < bottom - 1
95332
+ ) || titleBoxes.some(
95200
95333
  (box) => x >= box.x - TITLE_CLEARANCE && x <= box.x + box.width + TITLE_CLEARANCE && box.y < bottom - 1
95201
95334
  );
95202
95335
  var horizontalBlocked = (leafBoxes, y, probe) => leafBoxes.some(
@@ -95614,9 +95747,7 @@ function laneOffsets(lc, subset, direction, anchor) {
95614
95747
  const ownPosition = spanAnchor(lc.scene, { left, right, exempt }, direction, anchor) + direction * CHANNEL_GAP;
95615
95748
  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));
95616
95749
  labelHeights.push(labelHeight);
95617
- positions.push(
95618
- resolveLanePosition(lc, { left, right, exempt, labelHeight, start }, direction)
95619
- );
95750
+ positions.push(resolveLanePosition(lc, { left, right, exempt, labelHeight, start }, direction));
95620
95751
  }
95621
95752
  return positions;
95622
95753
  }
@@ -97030,6 +97161,7 @@ function clearRunHug(ctx, edge, run, own) {
97030
97161
  if (attemptHugFix(ctx, edge, { run, axis, target }, own)) return true;
97031
97162
  }
97032
97163
  if (run.i !== 0 && run.i + 1 !== edge.pts.length - 1) return false;
97164
+ if (edge.pinned?.[run.i === 0 ? "start" : "end"]) return false;
97033
97165
  if (!sideOf(edge.pts[run.i === 0 ? 0 : edge.pts.length - 1], leaves)) return false;
97034
97166
  return resideAtSide(ctx, edge, run, own);
97035
97167
  }
@@ -97462,8 +97594,10 @@ function generateReaimCandidates(AWAY_TOL2, from, to) {
97462
97594
  };
97463
97595
  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 };
97464
97596
  const candidates = [];
97465
- for (const srcSide of facingSides(src, dstC))
97466
- for (const dstSide of facingSides(dst, srcC)) {
97597
+ const srcSides = from.keepSide ? [from.keepSide] : facingSides(src, dstC);
97598
+ const dstSides = to.keepSide ? [to.keepSide] : facingSides(dst, srcC);
97599
+ for (const srcSide of srcSides)
97600
+ for (const dstSide of dstSides) {
97467
97601
  const na = normalOf(srcSide);
97468
97602
  const nb = normalOf(dstSide);
97469
97603
  const horizontalA = na.x !== 0;
@@ -97607,8 +97741,8 @@ function reaimEdge(rctx, edge) {
97607
97741
  const dstC = { x: dst.x + dst.width / 2, y: dst.y + dst.height / 2 };
97608
97742
  const candidates = generateReaimCandidates(
97609
97743
  AWAY_TOL2,
97610
- { node: src, centre: srcC },
97611
- { node: dst, centre: dstC }
97744
+ { node: src, centre: srcC, keepSide: edge.pinned?.start ? srcSeat.side : null },
97745
+ { node: dst, centre: dstC, keepSide: edge.pinned?.end ? dstSeat.side : null }
97612
97746
  );
97613
97747
  const outside = runsExcept(edge.id);
97614
97748
  const labelBoxes = seatedLabelBoxes(edge);
@@ -97640,7 +97774,8 @@ function reaimEdge(rctx, edge) {
97640
97774
  function reaimWrapAroundTerminals(ctx) {
97641
97775
  const { scene } = ctx;
97642
97776
  const rctx = createReaimContext(ctx);
97643
- for (const edge of scene.edges) reaimEdge(rctx, edge);
97777
+ for (const edge of scene.edges)
97778
+ if (!(edge.pinned?.start && edge.pinned?.end)) reaimEdge(rctx, edge);
97644
97779
  }
97645
97780
  function shiftCoincidentRun(leaves, pts, b, newAt) {
97646
97781
  if (b.vert) {
@@ -98086,8 +98221,10 @@ function bestUnweaveRoute(uctx, job) {
98086
98221
  const { edge, srcSeat, dstSeat } = job;
98087
98222
  let best = null;
98088
98223
  let bestLength = Number.POSITIVE_INFINITY;
98089
- for (const srcSide of ALL_SIDES)
98090
- for (const dstSide of ALL_SIDES)
98224
+ const srcSides = edge.pinned?.start ? [srcSeat.side] : ALL_SIDES;
98225
+ const dstSides = edge.pinned?.end ? [dstSeat.side] : ALL_SIDES;
98226
+ for (const srcSide of srcSides)
98227
+ for (const dstSide of dstSides)
98091
98228
  for (const srcOffset of SEAT_OFFSETS)
98092
98229
  for (const dstOffset of SEAT_OFFSETS) {
98093
98230
  const a = unweaveSeat(srcSeat.node, srcSide, srcOffset);
@@ -98140,6 +98277,7 @@ function unweaveAndClearContainers(ctx) {
98140
98277
  for (const edge of [...scene.edges].sort(
98141
98278
  (a, b) => (Number.parseInt(a.id.slice(1), 10) || 0) - (Number.parseInt(b.id.slice(1), 10) || 0)
98142
98279
  )) {
98280
+ if (edge.pinned?.start && edge.pinned?.end) continue;
98143
98281
  unweaveEdge(uctx, edge);
98144
98282
  }
98145
98283
  }
@@ -98445,7 +98583,7 @@ function createLaneModel(deps) {
98445
98583
  const fromB = lane - (firstVertical ? pts[3].y : pts[3].x);
98446
98584
  return fromA * fromB > 0;
98447
98585
  };
98448
- return { channelU, isChannelU };
98586
+ return { channelU, isChannelU, laneBeyond };
98449
98587
  }
98450
98588
  function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98451
98589
  const leaves = scene.nodes.filter((node) => !node.container);
@@ -98455,7 +98593,7 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98455
98593
  const preRouted = new Set(scene.edges.filter((edge) => edge.detour).map((edge) => edge.id));
98456
98594
  const { SIDES, seatOffsetsFor, seatOn, outward, horizontalSide } = createSeatModel(titleBoxes);
98457
98595
  const clock = { generation: 0 };
98458
- const { channelU, isChannelU } = createLaneModel({
98596
+ const { channelU, isChannelU, laneBeyond } = createLaneModel({
98459
98597
  scene,
98460
98598
  inspector,
98461
98599
  titleBoxes,
@@ -98465,7 +98603,10 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98465
98603
  const shapesFor = (a, b, sides, subject) => {
98466
98604
  const aH = horizontalSide(sides.a);
98467
98605
  const bH = horizontalSide(sides.b);
98468
- if (aH !== bH) return [[a, aH ? { x: b.x, y: a.y } : { x: a.x, y: b.y }, b]];
98606
+ if (aH !== bH) {
98607
+ const plainL = [a, aH ? { x: b.x, y: a.y } : { x: a.x, y: b.y }, b];
98608
+ return [plainL, ...approachLanes(a, b, sides, subject)];
98609
+ }
98469
98610
  if (outward(sides.a) === outward(sides.b)) return channelU(a, b, sides.a, subject);
98470
98611
  if (aH)
98471
98612
  return [
@@ -98479,6 +98620,40 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98479
98620
  Math.max(a.y, b.y) - CHANNEL_INSET
98480
98621
  ].map((mid) => [a, { x: a.x, y: mid }, { x: b.x, y: mid }, b]);
98481
98622
  };
98623
+ const approachLanes = (a, b, sides, subject) => {
98624
+ if (!subject.edge.pinned) return [];
98625
+ const aH = horizontalSide(sides.a);
98626
+ const bH = horizontalSide(sides.b);
98627
+ const beforeA = sides.a === "west" || sides.a === "north";
98628
+ const crossLane = laneBeyond(
98629
+ {
98630
+ spanLo: Math.min(a.x, b.x),
98631
+ spanHi: Math.max(a.x, b.x),
98632
+ start: (aH ? a.x : a.y) + (beforeA ? -CHANNEL_STEP : CHANNEL_STEP),
98633
+ vertical: !aH,
98634
+ before: beforeA
98635
+ },
98636
+ subject,
98637
+ true
98638
+ );
98639
+ if (crossLane === null) return [];
98640
+ const beforeB = sides.b === "west" || sides.b === "north";
98641
+ const search = {
98642
+ spanLo: bH ? Math.min(crossLane, b.y) : Math.min(crossLane, b.x),
98643
+ spanHi: bH ? Math.max(crossLane, b.y) : Math.max(crossLane, b.x),
98644
+ start: (bH ? b.x : b.y) + (beforeB ? -CHANNEL_STEP : CHANNEL_STEP),
98645
+ // `laneBeyond` names the axis after the terminal legs: a lane at a
98646
+ // constant x is the `vertical: false` search.
98647
+ vertical: !bH,
98648
+ before: beforeB
98649
+ };
98650
+ const lanes = [laneBeyond(search, subject, false), laneBeyond(search, subject, true)].filter(
98651
+ (lane, index, all) => lane !== null && all.indexOf(lane) === index
98652
+ );
98653
+ return lanes.map(
98654
+ (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]
98655
+ );
98656
+ };
98482
98657
  const routeCache = /* @__PURE__ */ new Map();
98483
98658
  const routesFor = (edge) => {
98484
98659
  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}`;
@@ -98489,8 +98664,10 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98489
98664
  const subject = { ends, edge };
98490
98665
  const out = [];
98491
98666
  const seen = /* @__PURE__ */ new Set();
98492
- for (const aSide of SIDES)
98493
- for (const bSide of SIDES) {
98667
+ const aSides = edge.pinned?.start ? [ends[0].side] : SIDES;
98668
+ const bSides = edge.pinned?.end ? [ends[1].side] : SIDES;
98669
+ for (const aSide of aSides)
98670
+ for (const bSide of bSides) {
98494
98671
  const sides = { a: aSide, b: bSide };
98495
98672
  for (const aOff of seatOffsetsFor(ends[0].node, aSide))
98496
98673
  for (const bOff of seatOffsetsFor(ends[1].node, bSide))
@@ -98822,13 +98999,18 @@ function computeIngressExternalElements(model) {
98822
98999
  }
98823
99000
  return ingressExternalElements;
98824
99001
  }
98825
- function toElkNode2(element, compact, containerFontSize, nodeFontSize) {
99002
+ var orderOption = (element) => element.order ? { "elk.position": `(0,${element.order.value})` } : {};
99003
+ var semiInteractiveOption = (children) => children.some((child) => child.order) ? { "elk.layered.crossingMinimization.semiInteractive": "true" } : {};
99004
+ function toElkNode2(element, compact, fonts, root = false) {
99005
+ const { cont: containerFontSize, node: nodeFontSize } = fonts;
98826
99006
  if (element.children.length) {
98827
99007
  const lineCount = (element.label ?? element.id).split("\n").length;
98828
99008
  return {
98829
99009
  id: element.id,
98830
99010
  layoutOptions: {
98831
- "elk.padding": `[top=${(compact ? 11 : 13) + lineCount * 14},left=${compact ? 7 : 9},bottom=${compact ? 7 : 9},right=${compact ? 7 : 9}]`
99011
+ "elk.padding": `[top=${(compact ? 11 : 13) + lineCount * 14},left=${compact ? 7 : 9},bottom=${compact ? 7 : 9},right=${compact ? 7 : 9}]`,
99012
+ ...root ? {} : orderOption(element),
99013
+ ...semiInteractiveOption(element.children)
98832
99014
  },
98833
99015
  labels: [
98834
99016
  {
@@ -98837,7 +99019,7 @@ function toElkNode2(element, compact, containerFontSize, nodeFontSize) {
98837
99019
  }
98838
99020
  ],
98839
99021
  children: element.children.map(
98840
- (child) => toElkNode2(child, compact, containerFontSize, nodeFontSize)
99022
+ (child) => toElkNode2(child, compact, fonts)
98841
99023
  )
98842
99024
  };
98843
99025
  }
@@ -98845,6 +99027,7 @@ function toElkNode2(element, compact, containerFontSize, nodeFontSize) {
98845
99027
  const isActor = element.kind === "actor";
98846
99028
  return {
98847
99029
  id: element.id,
99030
+ ...element.order && !root ? { layoutOptions: orderOption(element) } : {},
98848
99031
  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)),
98849
99032
  height: isActor ? 54 + ((element.label ?? element.id).split("\n").length - 1) * 11 : Math.max(compact ? 36 : 38, measured.height + (compact ? 10 : 12))
98850
99033
  };
@@ -98931,6 +99114,12 @@ function transpose(scene, titleBoxes) {
98931
99114
  }
98932
99115
  [scene.width, scene.height] = [scene.height, scene.width];
98933
99116
  }
99117
+ var PORT_PASS_ROUNDS = 2;
99118
+ function beatsRelayout(round, best) {
99119
+ if (!best) return true;
99120
+ if (round.tier !== best.tier) return round.tier < best.tier;
99121
+ return round.detours < best.detours;
99122
+ }
98934
99123
  function attachAwayOf(scene, model) {
98935
99124
  const ATTACH_AWAY_TOL = 24;
98936
99125
  const byId = new Map(scene.nodes.map((node) => [node.id, node]));
@@ -98941,21 +99130,35 @@ function attachAwayOf(scene, model) {
98941
99130
  const from = byId.get(flow?.from ?? "");
98942
99131
  const to = byId.get(flow?.to ?? "");
98943
99132
  if (!from || !to) continue;
98944
- const centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
99133
+ const centerOf2 = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
98945
99134
  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;
98946
99135
  const p0 = e.pts[0];
98947
99136
  const p1 = e.pts[1];
98948
99137
  const pn = e.pts[e.pts.length - 1];
98949
99138
  const pm = e.pts[e.pts.length - 2];
98950
- const toCenter = centerOf(to);
98951
- const fromCenter = centerOf(from);
98952
- if (away({ x: p1.x - p0.x, y: p1.y - p0.y }, { x: toCenter.x - p0.x, y: toCenter.y - p0.y }))
99139
+ const toCenter = centerOf2(to);
99140
+ const fromCenter = centerOf2(from);
99141
+ 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 }))
98953
99142
  flagged.add(e.id);
98954
- if (away({ x: pn.x - pm.x, y: pn.y - pm.y }, { x: pn.x - fromCenter.x, y: pn.y - fromCenter.y }))
99143
+ 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 }))
98955
99144
  flagged.add(e.id);
99145
+ if (!e.pinned && isLongDetour(e.pts, from, to)) flagged.add(e.id);
98956
99146
  }
98957
99147
  return flagged;
98958
99148
  }
99149
+ function longDetourCount(scene, model) {
99150
+ const byId = new Map(scene.nodes.map((node) => [node.id, node]));
99151
+ let count = 0;
99152
+ for (const edge of scene.edges) {
99153
+ if (edge.pts.length < 2) continue;
99154
+ const flow = model.flows.find((f) => f.id === edge.id);
99155
+ const from = byId.get(flow?.from ?? "");
99156
+ const to = byId.get(flow?.to ?? "");
99157
+ if (!from || !to) continue;
99158
+ if (isLongDetour(edge.pts, from, to)) count++;
99159
+ }
99160
+ return count;
99161
+ }
98959
99162
  function selectionExtras(scene, model) {
98960
99163
  const extra = /* @__PURE__ */ new Map();
98961
99164
  const edgeEnds = new Map(model.flows.map((flow) => [flow.id, /* @__PURE__ */ new Set([flow.from, flow.to])]));
@@ -99013,6 +99216,28 @@ function relayoutVerdict(before, after) {
99013
99216
  }
99014
99217
  return -1;
99015
99218
  }
99219
+ var centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
99220
+ function sideToward(from, to) {
99221
+ const dx = centerOf(to).x - centerOf(from).x;
99222
+ const dy = centerOf(to).y - centerOf(from).y;
99223
+ return Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? "WEST" : "EAST" : dy < 0 ? "NORTH" : "SOUTH";
99224
+ }
99225
+ function sideOn(p, n) {
99226
+ if (p.x > n.x - 2 && p.x < n.x + n.width + 2) {
99227
+ if (Math.abs(p.y - n.y) < 2) return "NORTH";
99228
+ if (Math.abs(p.y - (n.y + n.height)) < 2) return "SOUTH";
99229
+ }
99230
+ if (p.y > n.y - 2 && p.y < n.y + n.height + 2) {
99231
+ if (Math.abs(p.x - n.x) < 2) return "WEST";
99232
+ if (Math.abs(p.x - (n.x + n.width)) < 2) return "EAST";
99233
+ }
99234
+ return null;
99235
+ }
99236
+ function firstPassSide(scene, flowId, role, sceneNode) {
99237
+ const edge = scene.edges.find((candidate) => candidate.id === flowId);
99238
+ if (!edge || edge.pts.length < 2) return null;
99239
+ return sideOn(role === "src" ? edge.pts[0] : edge.pts[edge.pts.length - 1], sceneNode);
99240
+ }
99016
99241
  function constrainPorts(graph, scene, flagged, model) {
99017
99242
  const nodeById = new Map(scene.nodes.map((node) => [node.id, node]));
99018
99243
  const elkById = /* @__PURE__ */ new Map();
@@ -99021,23 +99246,6 @@ function constrainPorts(graph, scene, flagged, model) {
99021
99246
  for (const child of node.children ?? []) register(child);
99022
99247
  };
99023
99248
  register(graph);
99024
- const centerOf = (n) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 });
99025
- const sideToward = (from, to) => {
99026
- const dx = centerOf(to).x - centerOf(from).x;
99027
- const dy = centerOf(to).y - centerOf(from).y;
99028
- return Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? "WEST" : "EAST" : dy < 0 ? "NORTH" : "SOUTH";
99029
- };
99030
- const sideOn = (p, n) => {
99031
- if (p.x > n.x - 2 && p.x < n.x + n.width + 2) {
99032
- if (Math.abs(p.y - n.y) < 2) return "NORTH";
99033
- if (Math.abs(p.y - (n.y + n.height)) < 2) return "SOUTH";
99034
- }
99035
- if (p.y > n.y - 2 && p.y < n.y + n.height + 2) {
99036
- if (Math.abs(p.x - n.x) < 2) return "WEST";
99037
- if (Math.abs(p.x - (n.x + n.width)) < 2) return "EAST";
99038
- }
99039
- return null;
99040
- };
99041
99249
  const flaggedNodes = /* @__PURE__ */ new Set();
99042
99250
  for (const flow of model.flows)
99043
99251
  if (flagged.has(flow.id)) {
@@ -99054,14 +99262,9 @@ function constrainPorts(graph, scene, flagged, model) {
99054
99262
  const role = flow.from === nodeId ? "src" : "dst";
99055
99263
  const other = nodeById.get(role === "src" ? flow.to : flow.from);
99056
99264
  if (!other) continue;
99057
- let side = sideToward(sceneNode, other);
99058
- if (!flagged.has(flow.id)) {
99059
- const edge = scene.edges.find((e) => e.id === flow.id);
99060
- const terminal = edge && edge.pts.length >= 2 ? role === "src" ? edge.pts[0] : edge.pts[edge.pts.length - 1] : null;
99061
- const actual = terminal ? sideOn(terminal, sceneNode) : null;
99062
- if (actual) side = actual;
99063
- }
99265
+ const side = flagged.has(flow.id) ? sideToward(sceneNode, other) : firstPassSide(scene, flow.id, role, sceneNode) ?? sideToward(sceneNode, other);
99064
99266
  const portId = `${flow.id}${role === "src" ? "#out" : "#in"}`;
99267
+ if ((elkNode.ports ?? []).some((port) => port.id === portId)) continue;
99065
99268
  ports.push({ id: portId, width: 1, height: 1, layoutOptions: { "elk.port.side": side } });
99066
99269
  const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
99067
99270
  if (elkEdge) {
@@ -99077,6 +99280,7 @@ function constrainPorts(graph, scene, flagged, model) {
99077
99280
  var INGRESS_PARTITION = -1;
99078
99281
  var EGRESS_PARTITION = 900;
99079
99282
  var COMPACT_WRAP = 10;
99283
+ var SLOT_SCALE = 1e3;
99080
99284
  function elkPartitionOf(element, index, view, ingressExternal) {
99081
99285
  if (!view.partitionByOrder) return view.partitions[element.kind] ?? 1;
99082
99286
  if (element.kind === "actor" || element.kind === "actor-group") return INGRESS_PARTITION;
@@ -99085,6 +99289,52 @@ function elkPartitionOf(element, index, view, ingressExternal) {
99085
99289
  if (view.partitions[element.kind] !== void 0) return 90 + view.partitions[element.kind];
99086
99290
  return index;
99087
99291
  }
99292
+ function rootAncestorOf(model, id) {
99293
+ let element = model.index.get(id);
99294
+ if (!element) return void 0;
99295
+ while (element.parent) element = element.parent;
99296
+ return element.id;
99297
+ }
99298
+ function readingSlots(model, view, ingressExternal) {
99299
+ const slotOf = /* @__PURE__ */ new Map();
99300
+ if (!model.elements.some((element) => element.order)) return slotOf;
99301
+ const bandOf = new Map(
99302
+ model.elements.map((element, index) => [
99303
+ element.id,
99304
+ elkPartitionOf(element, index, view, ingressExternal)
99305
+ ])
99306
+ );
99307
+ const lowestOf = /* @__PURE__ */ new Map();
99308
+ for (const band of new Set(bandOf.values())) {
99309
+ const members = model.elements.filter((element) => bandOf.get(element.id) === band);
99310
+ const declared = [
99311
+ ...new Set(members.filter((m) => m.order).map((m) => m.order.value))
99312
+ ].sort((a, b) => a - b);
99313
+ const lowest = declared.length ? 1 : 0;
99314
+ lowestOf.set(band, lowest);
99315
+ for (const member of members)
99316
+ slotOf.set(
99317
+ member.id,
99318
+ member.order ? declared.indexOf(member.order.value) + 1 : lowest
99319
+ );
99320
+ }
99321
+ const ordered = new Set(model.elements.filter((element) => element.order).map((e) => e.id));
99322
+ for (let pass = 0; pass < model.elements.length; pass++) {
99323
+ let moved = false;
99324
+ for (const flow of model.flows) {
99325
+ const from = rootAncestorOf(model, flow.from);
99326
+ const to = rootAncestorOf(model, flow.to);
99327
+ if (!from || !to || from === to || ordered.has(to)) continue;
99328
+ if (bandOf.get(from) !== bandOf.get(to)) continue;
99329
+ const candidate = slotOf.get(from);
99330
+ if (candidate <= slotOf.get(to)) continue;
99331
+ slotOf.set(to, candidate);
99332
+ moved = true;
99333
+ }
99334
+ if (!moved) break;
99335
+ }
99336
+ return slotOf;
99337
+ }
99088
99338
  function elkFlowEdge(flow, ctx, labelWrap) {
99089
99339
  const { compact, numbered, fonts, businessObjectName } = ctx;
99090
99340
  if (numbered)
@@ -99121,9 +99371,88 @@ function elkFlowEdge(flow, ctx, labelWrap) {
99121
99371
  labels: text || chips.length ? [{ text, ...labelBox }] : []
99122
99372
  };
99123
99373
  }
99374
+ var SIDE_TO_ELK = {
99375
+ left: "WEST",
99376
+ right: "EAST",
99377
+ top: "NORTH",
99378
+ bottom: "SOUTH"
99379
+ };
99380
+ function applyDeclaredPorts(graph, model) {
99381
+ const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide);
99382
+ if (!pinned.length) return;
99383
+ const elkById = /* @__PURE__ */ new Map();
99384
+ const register = (node) => {
99385
+ elkById.set(node.id, node);
99386
+ for (const child of node.children ?? []) register(child);
99387
+ };
99388
+ register(graph);
99389
+ for (const flow of pinned) {
99390
+ const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
99391
+ if (!elkEdge) continue;
99392
+ for (const [role, declared, nodeId] of [
99393
+ ["out", flow.fromSide, flow.from],
99394
+ ["in", flow.toSide, flow.to]
99395
+ ]) {
99396
+ if (!declared) continue;
99397
+ const elkNode = elkById.get(nodeId);
99398
+ if (!elkNode) continue;
99399
+ const portId = `${flow.id}#${role}`;
99400
+ elkNode.ports = [
99401
+ ...elkNode.ports ?? [],
99402
+ {
99403
+ id: portId,
99404
+ width: 1,
99405
+ height: 1,
99406
+ layoutOptions: { "elk.port.side": SIDE_TO_ELK[declared.value] }
99407
+ }
99408
+ ];
99409
+ elkNode.layoutOptions = { ...elkNode.layoutOptions, "elk.portConstraints": "FIXED_SIDE" };
99410
+ if (role === "out") elkEdge.sources = [portId];
99411
+ else elkEdge.targets = [portId];
99412
+ }
99413
+ }
99414
+ }
99415
+ function terminalSide(p, node) {
99416
+ if (p.x > node.x - 2 && p.x < node.x + node.width + 2) {
99417
+ if (Math.abs(p.y - node.y) < 2) return "top";
99418
+ if (Math.abs(p.y - (node.y + node.height)) < 2) return "bottom";
99419
+ }
99420
+ if (p.y > node.y - 2 && p.y < node.y + node.height + 2) {
99421
+ if (Math.abs(p.x - node.x) < 2) return "left";
99422
+ if (Math.abs(p.x - (node.x + node.width)) < 2) return "right";
99423
+ }
99424
+ return null;
99425
+ }
99426
+ function attachSideDiagnostics(scene, model) {
99427
+ const diagnostics = [];
99428
+ const nodeById = new Map(scene.nodes.map((node) => [node.id, node]));
99429
+ for (const flow of model.flows) {
99430
+ if (!flow.fromSide && !flow.toSide) continue;
99431
+ const edge = scene.edges.find((candidate) => candidate.id === flow.id);
99432
+ if (!edge || edge.pts.length < 2) continue;
99433
+ for (const [declared, nodeId, point, role] of [
99434
+ [flow.fromSide, flow.from, edge.pts[0], "leaves"],
99435
+ [flow.toSide, flow.to, edge.pts[edge.pts.length - 1], "arrives on"]
99436
+ ]) {
99437
+ const node = declared ? nodeById.get(nodeId) : void 0;
99438
+ if (!declared || !node) continue;
99439
+ const actual = terminalSide(point, node);
99440
+ if (actual === declared.value) continue;
99441
+ diagnostics.push({
99442
+ code: "W0570",
99443
+ severity: "warning",
99444
+ message: `attachment side \`${declared.value}\` could not be honored`,
99445
+ span: declared.span,
99446
+ note: actual ? `the flow ${role} the ${actual} side of \`${nodeId}\`` : `the flow does not ${role} a side of \`${nodeId}\` cleanly`,
99447
+ help: "a side the layout cannot reach is dropped rather than forced \u2014 try the opposite endpoint, or `order:` to move the element instead"
99448
+ });
99449
+ }
99450
+ }
99451
+ return diagnostics;
99452
+ }
99124
99453
  function buildElkGraph(ctx, direction, options) {
99125
- const { model, view, ingressExternal, compact, numbered, fonts } = ctx;
99126
- return {
99454
+ const { model, view, ingressExternal, slotOf, compact, numbered, fonts } = ctx;
99455
+ const graph = {
99127
99456
  id: "root",
99128
99457
  layoutOptions: {
99129
99458
  "elk.algorithm": "layered",
@@ -99161,17 +99490,23 @@ function buildElkGraph(ctx, direction, options) {
99161
99490
  } : {}
99162
99491
  },
99163
99492
  children: model.elements.map((element, index) => {
99164
- const elkNode = toElkNode2(element, compact, fonts.cont, fonts.node);
99493
+ const elkNode = toElkNode2(element, compact, fonts, true);
99494
+ const band = elkPartitionOf(element, index, view, ingressExternal);
99495
+ const slot = slotOf.get(element.id);
99165
99496
  elkNode.layoutOptions = {
99166
99497
  ...elkNode.layoutOptions,
99498
+ // Without an `order:` anywhere the band is emitted as it always was, so
99499
+ // the drawing is byte-identical to one from before the hint existed.
99167
99500
  "elk.partitioning.partition": String(
99168
- elkPartitionOf(element, index, view, ingressExternal)
99501
+ slot === void 0 ? band : band * SLOT_SCALE + slot
99169
99502
  )
99170
99503
  };
99171
99504
  return elkNode;
99172
99505
  }),
99173
99506
  edges: model.flows.map((flow) => elkFlowEdge(flow, ctx, options?.labelWrap))
99174
99507
  };
99508
+ applyDeclaredPorts(graph, model);
99509
+ return graph;
99175
99510
  }
99176
99511
  function recordRepairs(scene, routesBefore) {
99177
99512
  for (const edge of scene.edges) {
@@ -99226,6 +99561,7 @@ async function layout(model, view) {
99226
99561
  model,
99227
99562
  view,
99228
99563
  ingressExternal: ingressExternalElements,
99564
+ slotOf: readingSlots(model, view, ingressExternalElements),
99229
99565
  compact,
99230
99566
  numbered,
99231
99567
  fonts: {
@@ -99246,18 +99582,25 @@ async function layout(model, view) {
99246
99582
  const nodes = walkedNodes.map((walked) => walked.node);
99247
99583
  for (const walked of walkedNodes) origins[walked.id] = { x: walked.x, y: walked.y };
99248
99584
  const edges = collectSceneEdges(result2, origins, numbered, edgeFontSize);
99249
- const scene2 = {
99585
+ const pinnedFlows = new Map(
99586
+ model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
99587
+ );
99588
+ for (const edge of edges) {
99589
+ const pinned = pinnedFlows.get(edge.id);
99590
+ if (pinned) edge.pinned = { ...pinned };
99591
+ }
99592
+ const scene = {
99250
99593
  width: Math.ceil(result2.width),
99251
99594
  height: Math.ceil(result2.height),
99252
99595
  nodes,
99253
99596
  edges,
99254
99597
  layoutMs: layoutMs2
99255
99598
  };
99256
- runGeometryPasses(scene2, model, {
99599
+ runGeometryPasses(scene, model, {
99257
99600
  numbered,
99258
99601
  sideways: disposition === "page" || disposition === "tall"
99259
99602
  });
99260
- return scene2;
99603
+ return scene;
99261
99604
  };
99262
99605
  const startTime = Date.now();
99263
99606
  let result;
@@ -99303,28 +99646,37 @@ async function layout(model, view) {
99303
99646
  result = await elk.layout(makeGraph(winnerDirection));
99304
99647
  }
99305
99648
  const layoutMs = Date.now() - startTime;
99306
- const scene = sceneFromResult(result, layoutMs);
99307
- const away = attachAwayOf(scene, model);
99649
+ const base = sceneFromResult(result, layoutMs);
99308
99650
  const skipPortPass = !!globalThis.process?.env?.CAIRN_NO_PORT_PASS;
99309
- if (!away.size || skipPortPass) return scene;
99310
- try {
99311
- const constrained = makeGraph(winnerDirection, winnerOptions);
99312
- constrainPorts(constrained, scene, away, model);
99313
- const reresult = await elk.layout(constrained);
99314
- const rescene = sceneFromResult(reresult, Date.now() - startTime);
99315
- const allEdges = (s) => new Set(s.edges.map((edge) => edge.id));
99316
- const before = inspect(scene, titleBoxesOf(scene, model)).local(allEdges(scene), /* @__PURE__ */ new Map());
99317
- const after = inspect(rescene, titleBoxesOf(rescene, model)).local(
99318
- allEdges(rescene),
99319
- /* @__PURE__ */ new Map()
99320
- );
99321
- for (const [key, tier] of selectionExtras(scene, model)) before.set(key, tier);
99322
- for (const [key, tier] of selectionExtras(rescene, model)) after.set(key, tier);
99323
- const verdict = relayoutVerdict(before, after);
99324
- return verdict >= 0 ? rescene : scene;
99325
- } catch {
99326
- return scene;
99651
+ if (skipPortPass) return base;
99652
+ const layoutProfile = (candidate) => {
99653
+ const everyEdge = new Set(candidate.edges.map((edge) => edge.id));
99654
+ const profile = inspect(candidate, titleBoxesOf(candidate, model)).local(everyEdge, /* @__PURE__ */ new Map());
99655
+ for (const [key, tier] of selectionExtras(candidate, model)) profile.set(key, tier);
99656
+ return profile;
99657
+ };
99658
+ const baseProfile = layoutProfile(base);
99659
+ let current = base;
99660
+ let best = null;
99661
+ for (let round = 0; round < PORT_PASS_ROUNDS; round++) {
99662
+ const flagged = attachAwayOf(current, model);
99663
+ if (!flagged.size) break;
99664
+ let candidate;
99665
+ try {
99666
+ const constrained = makeGraph(winnerDirection, winnerOptions);
99667
+ constrainPorts(constrained, current, flagged, model);
99668
+ const reresult = await elk.layout(constrained);
99669
+ candidate = sceneFromResult(reresult, Date.now() - startTime);
99670
+ } catch {
99671
+ break;
99672
+ }
99673
+ const tier = relayoutVerdict(baseProfile, layoutProfile(candidate));
99674
+ if (tier < 0) break;
99675
+ const scored = { scene: candidate, tier, detours: longDetourCount(candidate, model) };
99676
+ if (beatsRelayout(scored, best)) best = scored;
99677
+ current = candidate;
99327
99678
  }
99679
+ return best?.scene ?? base;
99328
99680
  }
99329
99681
 
99330
99682
  // src/localization.ts
@@ -99980,7 +100332,7 @@ function createEdgePainter(paint) {
99980
100332
  const flowStyle = flow?.style;
99981
100333
  const color = flowColorOf(flow);
99982
100334
  const headColor = style.flowColor === "by-source" ? color : defaultEdgeColor;
99983
- const dash = dashArray(flowStyle?.stroke?.style ?? style.flowStroke.style);
100335
+ const dash = dashArray(flowStyle?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style);
99984
100336
  const width = flowStyle?.stroke?.width ?? style.flowStroke.width;
99985
100337
  return `<path d="${edgePath(edge.pts)}" fill="none" stroke="${escAttr(color)}" stroke-width="${width}"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(headColor)})"/>
99986
100338
  `;
@@ -100311,6 +100663,7 @@ async function compile(source, options) {
100311
100663
  const view = views[model.type];
100312
100664
  const matrix = options?.matrix ? buildFlowMatrix(model, view) : null;
100313
100665
  const scene = await layout(model, view);
100666
+ diags.push(...attachSideDiagnostics(scene, model));
100314
100667
  const { svg, overlapsAfter } = render(model, view, scene);
100315
100668
  return {
100316
100669
  svg,
@@ -100326,7 +100679,7 @@ async function compile(source, options) {
100326
100679
  }
100327
100680
 
100328
100681
  // src/api.ts
100329
- var version = true ? "1.0.0-RC12" : pkg.version;
100682
+ var version = true ? "1.0.0-RC13" : pkg.version;
100330
100683
 
100331
100684
  // src/playground.ts
100332
100685
  var ElkClass = import_elk_bundled.default;