@r0kshan/cairn 1.0.0-RC14 → 1.0.0-RC15

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.
Files changed (3) hide show
  1. package/bin/cairn.mjs +405 -132
  2. package/dist/cairn.mjs +395 -125
  3. package/package.json +1 -1
package/bin/cairn.mjs CHANGED
@@ -92340,11 +92340,12 @@ var init_lexer = __esm({
92340
92340
  });
92341
92341
 
92342
92342
  // src/models/ast.ts
92343
- var ATTACH_SIDES, defaultDiagramStyle, explanations;
92343
+ var ATTACH_SIDES, ATTACH_ROLES, defaultDiagramStyle, explanations;
92344
92344
  var init_ast = __esm({
92345
92345
  "src/models/ast.ts"() {
92346
92346
  "use strict";
92347
92347
  ATTACH_SIDES = ["left", "right", "top", "bottom"];
92348
+ ATTACH_ROLES = ["producer", "consumer"];
92348
92349
  defaultDiagramStyle = () => ({
92349
92350
  crossingHops: true,
92350
92351
  compact: false,
@@ -92384,7 +92385,9 @@ var init_ast = __esm({
92384
92385
  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).",
92385
92386
  E0216: "Network zones belong to a site (or nest inside a larger zone): DMZ and LAN only mean something relative to a perimeter.",
92386
92387
  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.',
92387
- 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.",
92388
+ E0223: "Unknown endpoint suffix. 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`. It may instead name its role towards a queue at the other end: `producer` or `consumer`. A declared side is a hint, not a guarantee: one the layout cannot reach is dropped with a W0570 warning rather than forced.",
92389
+ E0224: "An endpoint role says what this element does with the *queue* at the other end of the flow (`CAPTURE.producer -> EVENTS`), so the other end has to be one. Point the flow at a queue, or drop the role \u2014 between two ordinary elements the arrow already says everything a role would.",
92390
+ E0225: "A flow's queue endpoint carries both a declared side and a role at the other end, and they are two answers to one question: `producer` already means the queue's left cap and `consumer` its right. Keep the role and drop the side, or keep the side and drop the role.",
92388
92391
  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.",
92389
92392
  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.',
92390
92393
  E0222: "Business objects are a logical-view concept (what data circulates between functional blocks). They are not part of the application or infrastructure 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`.",
@@ -93136,56 +93139,60 @@ function parseElement(p, sourceToken, parent) {
93136
93139
  }
93137
93140
  (parent ? parent.children : model.elements).push(element);
93138
93141
  }
93139
- function splitEndpoint(token, flow, role) {
93142
+ function splitEndpoint(token, flow, end) {
93140
93143
  const dot = token.text.lastIndexOf(".");
93141
93144
  if (dot <= 0 || dot === token.text.length - 1) return null;
93142
93145
  const base = token.text.slice(0, dot);
93143
- const side = token.text.slice(dot + 1);
93146
+ const suffix = token.text.slice(dot + 1);
93144
93147
  return {
93145
93148
  flow,
93146
- role,
93149
+ end,
93147
93150
  raw: token.text,
93148
93151
  rawSpan: token.span,
93149
93152
  base,
93150
93153
  baseSpan: { line: token.span.line, col: token.span.col, len: base.length },
93151
- side,
93152
- sideSpan: { line: token.span.line, col: token.span.col + dot + 1, len: side.length }
93154
+ suffix,
93155
+ suffixSpan: { line: token.span.line, col: token.span.col + dot + 1, len: suffix.length }
93153
93156
  };
93154
93157
  }
93155
- function resolveAttachSides(splits, model, diagnostics) {
93158
+ function resolveEndpointSuffixes(splits, model, diagnostics) {
93156
93159
  for (const split of splits) {
93160
+ const isSide = ATTACH_SIDES.includes(split.suffix);
93161
+ const isRole = ATTACH_ROLES.includes(split.suffix);
93157
93162
  if (model.index.has(split.raw)) {
93158
- if (ATTACH_SIDES.includes(split.side))
93163
+ if (isSide || isRole)
93159
93164
  diagnostics.push({
93160
93165
  code: "W0571",
93161
93166
  severity: "warning",
93162
- message: `\`${split.raw}\` is a declared element, so \`.${split.side}\` is not read as an attachment side`,
93167
+ message: `\`${split.raw}\` is a declared element, so \`.${split.suffix}\` is not read as ${isSide ? "an attachment side" : "a role"}`,
93163
93168
  span: split.rawSpan,
93164
93169
  note: "a declared id always wins over the `ID.side` reading",
93165
- help: `rename the element if you meant to attach the flow to the ${split.side} side of \`${split.base}\``
93170
+ help: isSide ? `rename the element if you meant to attach the flow to the ${split.suffix} side of \`${split.base}\`` : `rename the element if you meant \`${split.base}\` to be the ${split.suffix} of the queue at the other end`
93166
93171
  });
93167
93172
  continue;
93168
93173
  }
93169
93174
  if (!model.index.has(split.base)) continue;
93170
- const known = ATTACH_SIDES.includes(split.side);
93171
- if (!known)
93175
+ if (!isSide && !isRole)
93172
93176
  diagnostics.push({
93173
93177
  code: "E0223",
93174
93178
  severity: "error",
93175
- message: `unknown attachment side \`${split.side}\``,
93176
- span: split.sideSpan,
93177
- note: "sides are named as the diagram is read",
93178
- help: "use `left`, `right`, `top` or `bottom`, e.g. `APP.right -> DB.left`"
93179
+ message: `unknown endpoint suffix \`${split.suffix}\``,
93180
+ span: split.suffixSpan,
93181
+ note: "an endpoint names either the side it attaches to, as the diagram is read, or its role towards a queue",
93182
+ help: "use `left`, `right`, `top` or `bottom` \u2014 `APP.right -> DB.left` \u2014 or `producer` / `consumer`, e.g. `CAPTURE.producer -> EVENTS`"
93179
93183
  });
93180
- const side = known ? { value: split.side, span: split.sideSpan } : void 0;
93181
- if (split.role === "from") {
93184
+ const side = isSide ? { value: split.suffix, span: split.suffixSpan } : void 0;
93185
+ const role = isRole ? { value: split.suffix, span: split.suffixSpan } : void 0;
93186
+ if (split.end === "from") {
93182
93187
  split.flow.from = split.base;
93183
93188
  split.flow.fromSpan = split.baseSpan;
93184
93189
  split.flow.fromSide = side;
93190
+ split.flow.fromRole = role;
93185
93191
  } else {
93186
93192
  split.flow.to = split.base;
93187
93193
  split.flow.toSpan = split.baseSpan;
93188
93194
  split.flow.toSide = side;
93195
+ split.flow.toRole = role;
93189
93196
  }
93190
93197
  }
93191
93198
  }
@@ -93462,7 +93469,7 @@ function parse(src) {
93462
93469
  for (const [id, element] of indexElementsById(model.elements)) {
93463
93470
  if (!model.index.has(id)) model.index.set(id, element);
93464
93471
  }
93465
- resolveAttachSides(endpointSplits, model, diagnostics);
93472
+ resolveEndpointSuffixes(endpointSplits, model, diagnostics);
93466
93473
  return { model, diags: diagnostics };
93467
93474
  }
93468
93475
  function applyStyleEntry(entry) {
@@ -93694,10 +93701,11 @@ var init_parser = __esm({
93694
93701
  });
93695
93702
 
93696
93703
  // src/views.ts
93697
- var logicalView, applicationView, infrastructureView, views;
93704
+ var LINE_STYLES2, logicalView, applicationView, infrastructureView, views;
93698
93705
  var init_views = __esm({
93699
93706
  "src/views.ts"() {
93700
93707
  "use strict";
93708
+ LINE_STYLES2 = ["solid", "dashed", "dotted"];
93701
93709
  logicalView = {
93702
93710
  name: "logical",
93703
93711
  laneKinds: ["external"],
@@ -93729,6 +93737,16 @@ var init_views = __esm({
93729
93737
  },
93730
93738
  legendFlowLabel: "Functional flow (label = exchanged data)",
93731
93739
  legendFlowLabelFr: "Flux fonctionnel (libell\xE9 = donn\xE9es \xE9chang\xE9es)",
93740
+ legendLineStyles: {
93741
+ solid: "Direct exchange",
93742
+ dashed: "Asynchronous or event-driven exchange",
93743
+ dotted: "Dependency \u2014 no data exchanged"
93744
+ },
93745
+ legendLineStylesFr: {
93746
+ solid: "\xC9change direct",
93747
+ dashed: "\xC9change asynchrone ou \xE9v\xE9nementiel",
93748
+ dotted: "D\xE9pendance \u2014 aucune donn\xE9e \xE9chang\xE9e"
93749
+ },
93732
93750
  flowTechRequired: null,
93733
93751
  flowTechRecommended: null,
93734
93752
  businessObjects: true,
@@ -93824,6 +93842,7 @@ var init_views = __esm({
93824
93842
  applicationView = {
93825
93843
  name: "application",
93826
93844
  laneKinds: ["external"],
93845
+ hubKinds: ["queue"],
93827
93846
  kinds: [
93828
93847
  "actor-group",
93829
93848
  "actor",
@@ -93900,6 +93919,16 @@ var init_views = __esm({
93900
93919
  },
93901
93920
  legendFlowLabel: "Application flow \u2014 (protocol, format) under the label",
93902
93921
  legendFlowLabelFr: "Flux applicatif \u2014 (protocole, format) sous le libell\xE9",
93922
+ legendLineStyles: {
93923
+ solid: "Synchronous call (request / response)",
93924
+ dashed: "Asynchronous exchange (message, event)",
93925
+ dotted: "Dependency \u2014 no direct call"
93926
+ },
93927
+ legendLineStylesFr: {
93928
+ solid: "Appel synchrone (requ\xEAte / r\xE9ponse)",
93929
+ dashed: "\xC9change asynchrone (message, \xE9v\xE9nement)",
93930
+ dotted: "D\xE9pendance \u2014 sans appel direct"
93931
+ },
93903
93932
  flowLabelRequired: null,
93904
93933
  flowTechRequired: null,
93905
93934
  flowTechRecommended: {
@@ -94023,6 +94052,7 @@ var init_views = __esm({
94023
94052
  infrastructureView = {
94024
94053
  name: "infrastructure",
94025
94054
  laneKinds: ["external"],
94055
+ hubKinds: ["queue"],
94026
94056
  kinds: [
94027
94057
  "actor",
94028
94058
  "device",
@@ -94085,6 +94115,16 @@ var init_views = __esm({
94085
94115
  },
94086
94116
  legendFlowLabel: "Technical flow (protocol, port)",
94087
94117
  legendFlowLabelFr: "Flux technique (protocole, port)",
94118
+ legendLineStyles: {
94119
+ solid: "Permanent link \u2014 nominal traffic",
94120
+ dashed: "Asynchronous or intermittent link",
94121
+ dotted: "Dependency \u2014 outside nominal traffic"
94122
+ },
94123
+ legendLineStylesFr: {
94124
+ solid: "Lien permanent \u2014 trafic nominal",
94125
+ dashed: "Lien asynchrone ou intermittent",
94126
+ dotted: "D\xE9pendance \u2014 hors trafic nominal"
94127
+ },
94088
94128
  flowLabelRequired: null,
94089
94129
  flowTechRecommended: null,
94090
94130
  flowTechRequired: {
@@ -94423,6 +94463,7 @@ function validate(model) {
94423
94463
  ...checkMissingLabels(elements),
94424
94464
  ...checkNesting(elements, view),
94425
94465
  ...checkFlows(model, view),
94466
+ ...checkEndpointRoles(model, view),
94426
94467
  ...checkLogos(elements, view),
94427
94468
  ...checkBusinessObjects(model, view),
94428
94469
  ...checkMinimumCounts(elements, model, view),
@@ -94541,6 +94582,42 @@ function checkNesting(elements, view) {
94541
94582
  }
94542
94583
  return diagnostics;
94543
94584
  }
94585
+ function checkEndpointRoles(model, view) {
94586
+ const diagnostics = [];
94587
+ const hubKinds = new Set(view.hubKinds ?? []);
94588
+ const isHub = (id) => hubKinds.has(model.index.get(id)?.kind ?? "");
94589
+ for (const flow of model.flows) {
94590
+ for (const [role, hubId, hubSide] of [
94591
+ [flow.fromRole, flow.to, flow.toSide],
94592
+ [flow.toRole, flow.from, flow.fromSide]
94593
+ ]) {
94594
+ if (!role) continue;
94595
+ if (!model.index.has(hubId)) continue;
94596
+ if (!isHub(hubId)) {
94597
+ const kinds = [...hubKinds];
94598
+ diagnostics.push({
94599
+ code: "E0224",
94600
+ severity: "error",
94601
+ message: `\`${role.value}\` needs a ${kinds.join(" or ") || "queue"} at the other end of the flow`,
94602
+ span: role.span,
94603
+ note: `\`${hubId}\` is ${model.index.get(hubId)?.kind ? `a \`${model.index.get(hubId).kind}\`` : "not one"}`,
94604
+ help: "a role says what this element does with a queue \u2014 between two ordinary elements the arrow already says it"
94605
+ });
94606
+ continue;
94607
+ }
94608
+ if (hubSide)
94609
+ diagnostics.push({
94610
+ code: "E0225",
94611
+ severity: "error",
94612
+ message: `\`${hubId}.${hubSide.value}\` and \`${role.value}\` both decide where this flow meets the queue`,
94613
+ span: hubSide.span,
94614
+ note: "`producer` means the queue's left cap and `consumer` its right",
94615
+ help: "keep the role and drop the side, or keep the side and drop the role"
94616
+ });
94617
+ }
94618
+ }
94619
+ return diagnostics;
94620
+ }
94544
94621
  function checkFlows(model, view) {
94545
94622
  const diagnostics = [];
94546
94623
  const isActor = (id) => {
@@ -96252,13 +96329,34 @@ function compactVertical(scene) {
96252
96329
  }
96253
96330
  scene.height = Math.ceil(heightAfter + bottomMargin);
96254
96331
  }
96255
- var KEEP_GAP, EDGE_MARGIN, MIN_SAVING;
96332
+ function fitCanvas(scene) {
96333
+ let maxX = 0;
96334
+ let maxY = 0;
96335
+ for (const node of scene.nodes) {
96336
+ maxX = Math.max(maxX, node.x + node.width);
96337
+ maxY = Math.max(maxY, node.y + node.height);
96338
+ }
96339
+ for (const edge of scene.edges) {
96340
+ for (const point of edge.pts) {
96341
+ maxX = Math.max(maxX, point.x);
96342
+ maxY = Math.max(maxY, point.y);
96343
+ }
96344
+ for (const label of edge.labels) {
96345
+ maxX = Math.max(maxX, label.x + label.width);
96346
+ maxY = Math.max(maxY, label.y + label.height);
96347
+ }
96348
+ }
96349
+ scene.width = Math.max(scene.width, Math.ceil(maxX) + CANVAS_MARGIN);
96350
+ scene.height = Math.max(scene.height, Math.ceil(maxY) + CANVAS_MARGIN);
96351
+ }
96352
+ var KEEP_GAP, EDGE_MARGIN, MIN_SAVING, CANVAS_MARGIN;
96256
96353
  var init_compact = __esm({
96257
96354
  "src/compact.ts"() {
96258
96355
  "use strict";
96259
96356
  KEEP_GAP = 14;
96260
96357
  EDGE_MARGIN = 22;
96261
96358
  MIN_SAVING = 4;
96359
+ CANVAS_MARGIN = 10;
96262
96360
  }
96263
96361
  });
96264
96362
 
@@ -98936,8 +99034,8 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98936
99034
  const subject = { ends, edge };
98937
99035
  const out = [];
98938
99036
  const seen = /* @__PURE__ */ new Set();
98939
- const aSides = edge.pinned?.start ? [ends[0].side] : SIDES;
98940
- const bSides = edge.pinned?.end ? [ends[1].side] : SIDES;
99037
+ const aSides = sideFixed(edge, "start") ? [ends[0].side] : SIDES;
99038
+ const bSides = sideFixed(edge, "end") ? [ends[1].side] : SIDES;
98941
99039
  for (const aSide of aSides)
98942
99040
  for (const bSide of bSides) {
98943
99041
  const sides = { a: aSide, b: bSide };
@@ -98996,12 +99094,13 @@ function optimiseRoutes(scene, titleBoxes = [], folded = false) {
98996
99094
  }
98997
99095
  for (const edge of scene.edges) if (edge.pts.length >= 2) enforceOrthogonal(edge);
98998
99096
  }
98999
- var ORTHOGONAL_EPSILON, MIN_HUG_SPAN, CHANNEL_INSET, SNAP, JOG_SNAP, SIDE_INSET, MIN_SIDE_INSET, FAN_REACH2, SEAT_OFFSETS, segmentsCross, segmentLength, wouldMerge, seatWithAlong, crossingsOf, SEAT_GRID, SEAT_CLEAR, CHANNEL_MARGIN, CHANNEL_STEP, CHANNEL_CLEAR;
99097
+ var sideFixed, ORTHOGONAL_EPSILON, MIN_HUG_SPAN, CHANNEL_INSET, SNAP, JOG_SNAP, SIDE_INSET, MIN_SIDE_INSET, FAN_REACH2, SEAT_OFFSETS, segmentsCross, segmentLength, wouldMerge, seatWithAlong, crossingsOf, SEAT_GRID, SEAT_CLEAR, CHANNEL_MARGIN, CHANNEL_STEP, CHANNEL_CLEAR;
99000
99098
  var init_edge_tidy = __esm({
99001
99099
  "src/edge-tidy.ts"() {
99002
99100
  "use strict";
99003
99101
  init_readability();
99004
99102
  init_geometry();
99103
+ sideFixed = (edge, end) => !!(edge.pinned?.[end] || edge.hubSided?.[end]);
99005
99104
  ORTHOGONAL_EPSILON = 0.5;
99006
99105
  MIN_HUG_SPAN = 24;
99007
99106
  CHANNEL_INSET = 24;
@@ -99660,8 +99759,7 @@ function constrainPorts(graph, scene, flagged, model) {
99660
99759
  }
99661
99760
  function elkPartitionOf(element, index, view, ingressExternal) {
99662
99761
  if (!view.partitionByOrder) return view.partitions[element.kind] ?? 1;
99663
- if ((view.ingressKinds ?? DEFAULT_INGRESS_KINDS).includes(element.kind))
99664
- return INGRESS_PARTITION;
99762
+ if ((view.ingressKinds ?? DEFAULT_INGRESS_KINDS).includes(element.kind)) return INGRESS_PARTITION;
99665
99763
  if (element.kind === "external")
99666
99764
  return ingressExternal.has(element.id) ? INGRESS_PARTITION : EGRESS_PARTITION;
99667
99765
  if (view.partitions[element.kind] !== void 0) return 90 + view.partitions[element.kind];
@@ -99710,13 +99808,15 @@ function readingSlots(model, view, ingressExternal) {
99710
99808
  }
99711
99809
  return slotOf;
99712
99810
  }
99811
+ function elkEnds(flow) {
99812
+ return laidOutReversed(flow) ? { sources: [flow.to], targets: [flow.from] } : { sources: [flow.from], targets: [flow.to] };
99813
+ }
99713
99814
  function elkFlowEdge(flow, ctx, labelWrap) {
99714
99815
  const { compact, numbered, fonts, businessObjectName } = ctx;
99715
99816
  if (numbered)
99716
99817
  return {
99717
99818
  id: flow.id,
99718
- sources: [flow.from],
99719
- targets: [flow.to],
99819
+ ...elkEnds(flow),
99720
99820
  labels: [
99721
99821
  {
99722
99822
  text: String(parseInt(flow.id.slice(1), 10)),
@@ -99741,13 +99841,35 @@ function elkFlowEdge(flow, ctx, labelWrap) {
99741
99841
  });
99742
99842
  return {
99743
99843
  id: flow.id,
99744
- sources: [flow.from],
99745
- targets: [flow.to],
99844
+ ...elkEnds(flow),
99746
99845
  labels: text || chips.length ? [{ text, ...labelBox }] : []
99747
99846
  };
99748
99847
  }
99749
- function applyDeclaredPorts(graph, model) {
99750
- const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide);
99848
+ function laidOutReversed(flow) {
99849
+ return flow.fromRole?.value === "consumer" || flow.toRole?.value === "producer";
99850
+ }
99851
+ function roleCap(role) {
99852
+ if (!role) return null;
99853
+ return role.value === "producer" ? "left" : "right";
99854
+ }
99855
+ function hubFlowSides(model, view) {
99856
+ const derived = /* @__PURE__ */ new Map();
99857
+ const hubKinds = new Set(view.hubKinds ?? []);
99858
+ if (!hubKinds.size) return derived;
99859
+ const hubs = new Set(
99860
+ indexElementsById(model.elements).filter(([, element]) => hubKinds.has(element.kind)).map(([id]) => id)
99861
+ );
99862
+ if (!hubs.size) return derived;
99863
+ for (const flow of model.flows) {
99864
+ const sides = {};
99865
+ if (hubs.has(flow.to) && !flow.toSide) sides.to = roleCap(flow.fromRole) ?? "left";
99866
+ if (hubs.has(flow.from) && !flow.fromSide) sides.from = roleCap(flow.toRole) ?? "right";
99867
+ if (sides.to || sides.from) derived.set(flow.id, sides);
99868
+ }
99869
+ return derived;
99870
+ }
99871
+ function applyDeclaredPorts(graph, model, derived) {
99872
+ const pinned = model.flows.filter((flow) => flow.fromSide || flow.toSide || derived.has(flow.id));
99751
99873
  if (!pinned.length) return;
99752
99874
  const elkById = /* @__PURE__ */ new Map();
99753
99875
  const register = (node) => {
@@ -99758,14 +99880,15 @@ function applyDeclaredPorts(graph, model) {
99758
99880
  for (const flow of pinned) {
99759
99881
  const elkEdge = (graph.edges ?? []).find((edge) => edge.id === flow.id);
99760
99882
  if (!elkEdge) continue;
99883
+ const hub = derived.get(flow.id);
99761
99884
  for (const [role, declared, nodeId] of [
99762
- ["out", flow.fromSide, flow.from],
99763
- ["in", flow.toSide, flow.to]
99885
+ ["out", flow.fromSide ?? sideRequest(hub?.from), flow.from],
99886
+ ["in", flow.toSide ?? sideRequest(hub?.to), flow.to]
99764
99887
  ]) {
99765
99888
  if (!declared) continue;
99766
99889
  const elkNode = elkById.get(nodeId);
99767
99890
  if (!elkNode) continue;
99768
- const portId = `${flow.id}#${role}`;
99891
+ const portId = `${flow.id}#${role === "out" !== laidOutReversed(flow) ? "out" : "in"}`;
99769
99892
  elkNode.ports = [
99770
99893
  ...elkNode.ports ?? [],
99771
99894
  {
@@ -99776,7 +99899,7 @@ function applyDeclaredPorts(graph, model) {
99776
99899
  }
99777
99900
  ];
99778
99901
  elkNode.layoutOptions = { ...elkNode.layoutOptions, "elk.portConstraints": "FIXED_SIDE" };
99779
- if (role === "out") elkEdge.sources = [portId];
99902
+ if (role === "out" !== laidOutReversed(flow)) elkEdge.sources = [portId];
99780
99903
  else elkEdge.targets = [portId];
99781
99904
  }
99782
99905
  }
@@ -99838,7 +99961,7 @@ function buildElkGraph(ctx, direction, options) {
99838
99961
  "elk.edgeRouting": "ORTHOGONAL",
99839
99962
  "elk.partitioning.activate": "true",
99840
99963
  "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
99841
- "elk.layered.compaction.postCompaction.strategy": "EDGE_LENGTH",
99964
+ "elk.layered.compaction.postCompaction.strategy": options?.postCompaction ?? "EDGE_LENGTH",
99842
99965
  "elk.layered.feedbackEdges": "true",
99843
99966
  "elk.layered.thoroughness": "30",
99844
99967
  "elk.separateConnectedComponents": "false",
@@ -99884,7 +100007,11 @@ function buildElkGraph(ctx, direction, options) {
99884
100007
  }),
99885
100008
  edges: model.flows.map((flow) => elkFlowEdge(flow, ctx, options?.labelWrap))
99886
100009
  };
99887
- applyDeclaredPorts(graph, model);
100010
+ applyDeclaredPorts(
100011
+ graph,
100012
+ model,
100013
+ options?.hubPorts === false ? /* @__PURE__ */ new Map() : hubFlowSides(model, view)
100014
+ );
99888
100015
  return graph;
99889
100016
  }
99890
100017
  function recordRepairs(scene, routesBefore) {
@@ -99953,7 +100080,9 @@ function snapLanes(scene, laneOf, axis) {
99953
100080
  if (!laneOf.size) return;
99954
100081
  const leaves = leafBoxesOf(scene);
99955
100082
  const tilted = (edges) => edges.reduce(
99956
- (total, edge) => total + edge.pts.filter((point, i) => i > 0 && Math.abs(point.x - edge.pts[i - 1].x) >= 0.5 && Math.abs(point.y - edge.pts[i - 1].y) >= 0.5).length,
100083
+ (total, edge) => total + edge.pts.filter(
100084
+ (point, i) => i > 0 && Math.abs(point.x - edge.pts[i - 1].x) >= 0.5 && Math.abs(point.y - edge.pts[i - 1].y) >= 0.5
100085
+ ).length,
99957
100086
  0
99958
100087
  );
99959
100088
  const touching = /* @__PURE__ */ new Map();
@@ -99997,7 +100126,9 @@ function snapLanes(scene, laneOf, axis) {
99997
100126
  point[axis] += delta;
99998
100127
  const broke = crossingsAround(scene, edges) > crossingsBefore || shortJogs(edges) > jogsBefore || tilted(edges) > tiltsBefore || edges.some((edge) => crossesLeaf(edge, leaves, attachedTo.get(edge.id) ?? /* @__PURE__ */ new Set())) || // Containers included: a lane member is top-level, so it has no
99999
100128
  // legitimate container ancestor and may not land on one.
100000
- scene.nodes.some((other) => other !== node && !node.container && other.x < node.x + node.width && node.x < other.x + other.width && other.y < node.y + node.height && node.y < other.y + other.height);
100129
+ scene.nodes.some(
100130
+ (other) => other !== node && !node.container && other.x < node.x + node.width && node.x < other.x + other.width && other.y < node.y + node.height && node.y < other.y + other.height
100131
+ );
100001
100132
  if (broke) {
100002
100133
  node[axis] = target - delta;
100003
100134
  edges.forEach((edge, i) => {
@@ -100032,6 +100163,7 @@ function runGeometryPasses(scene, model, options) {
100032
100163
  clearSideHugs(scene, settledTitles);
100033
100164
  anchorFlowLabels(scene, settledTitles);
100034
100165
  swapCrossingSiblingSeats(scene);
100166
+ fitCanvas(scene);
100035
100167
  }
100036
100168
  function laneAssignment(model, view) {
100037
100169
  const laneOf = /* @__PURE__ */ new Map();
@@ -100089,6 +100221,38 @@ function laneAssignment(model, view) {
100089
100221
  }
100090
100222
  return laneOf;
100091
100223
  }
100224
+ async function withHubPortFallback(elk, makeGraph, direction, options) {
100225
+ const run = async (spec) => await elk.layout(makeGraph(direction, spec));
100226
+ const rungs = options?.hubPorts === false ? [] : [
100227
+ { ...options, postCompaction: "EDGE_LENGTH_CONSTRAINT_LOCKING" },
100228
+ { ...options, postCompaction: "NONE" },
100229
+ { ...options, hubPorts: false }
100230
+ ];
100231
+ try {
100232
+ return { result: await run(options), options };
100233
+ } catch (error) {
100234
+ for (const [index, rung] of rungs.entries()) {
100235
+ try {
100236
+ return { result: await run(rung), options: rung };
100237
+ } catch (rungError) {
100238
+ if (index === rungs.length - 1) throw rungError;
100239
+ }
100240
+ }
100241
+ throw error;
100242
+ }
100243
+ }
100244
+ function markDeclaredTerminals(edges, model, view, hubPorts) {
100245
+ const pinnedFlows = new Map(
100246
+ model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
100247
+ );
100248
+ const hubSides = hubPorts ? hubFlowSides(model, view) : /* @__PURE__ */ new Map();
100249
+ for (const edge of edges) {
100250
+ const pinned = pinnedFlows.get(edge.id);
100251
+ if (pinned) edge.pinned = { ...pinned };
100252
+ const hub = hubSides.get(edge.id);
100253
+ if (hub) edge.hubSided = { start: !!hub.from, end: !!hub.to };
100254
+ }
100255
+ }
100092
100256
  async function layout(model, view) {
100093
100257
  const elk = await getElk();
100094
100258
  const businessObjectName = new Map(model.businessObjects.map((bo) => [bo.id, bo.name]));
@@ -100120,7 +100284,7 @@ async function layout(model, view) {
100120
100284
  };
100121
100285
  const makeGraph = (direction, options) => buildElkGraph(graphContext, direction, options);
100122
100286
  const kindOf = new Map(indexElementsById(model.elements));
100123
- const sceneFromResult = (result2, layoutMs2, lanes = true) => {
100287
+ const sceneFromResult = (result2, layoutMs2, laidOutWith, lanes = true) => {
100124
100288
  const origins = {
100125
100289
  root: { x: 0, y: 0 }
100126
100290
  };
@@ -100128,13 +100292,9 @@ async function layout(model, view) {
100128
100292
  const nodes = walkedNodes.map((walked) => walked.node);
100129
100293
  for (const walked of walkedNodes) origins[walked.id] = { x: walked.x, y: walked.y };
100130
100294
  const edges = collectSceneEdges(result2, origins, numbered, edgeFontSize);
100131
- const pinnedFlows = new Map(
100132
- model.flows.filter((flow) => flow.fromSide || flow.toSide).map((flow) => [flow.id, { start: !!flow.fromSide, end: !!flow.toSide }])
100133
- );
100134
- for (const edge of edges) {
100135
- const pinned = pinnedFlows.get(edge.id);
100136
- if (pinned) edge.pinned = { ...pinned };
100137
- }
100295
+ const reversed = new Set(model.flows.filter(laidOutReversed).map((flow) => flow.id));
100296
+ for (const edge of edges) if (reversed.has(edge.id)) edge.pts.reverse();
100297
+ markDeclaredTerminals(edges, model, view, laidOutWith?.hubPorts !== false);
100138
100298
  const scene = {
100139
100299
  width: Math.ceil(result2.width),
100140
100300
  height: Math.ceil(result2.height),
@@ -100151,6 +100311,7 @@ async function layout(model, view) {
100151
100311
  };
100152
100312
  const laneOf = laneAssignment(model, view);
100153
100313
  const startTime = Date.now();
100314
+ const layoutGraph = (direction, options) => withHubPortFallback(elk, makeGraph, direction, options);
100154
100315
  let result;
100155
100316
  let winnerDirection;
100156
100317
  let winnerOptions;
@@ -100161,9 +100322,10 @@ async function layout(model, view) {
100161
100322
  { direction: "RIGHT", options: { labelWrap: 14, tight: true } },
100162
100323
  { direction: "RIGHT", options: { labelWrap: 14, tight: true, minLayers: true } }
100163
100324
  ] : [{ direction: "DOWN" }, { direction: "DOWN", options: { labelWrap: 16 } }];
100164
- const candidates = await Promise.all(
100165
- graphSpecs.map((spec) => elk.layout(makeGraph(spec.direction, spec.options)))
100325
+ const laidOutSpecs = await Promise.all(
100326
+ graphSpecs.map((spec) => layoutGraph(spec.direction, spec.options))
100166
100327
  );
100328
+ const candidates = laidOutSpecs.map((laidOut) => laidOut.result);
100167
100329
  const preferWide = disposition === "slide";
100168
100330
  const orientedLayouts = candidates.map((layoutResult, index) => ({ layoutResult, index })).filter(
100169
100331
  ({ layoutResult }) => preferWide ? layoutResult.width >= layoutResult.height : layoutResult.height >= layoutResult.width
@@ -100176,7 +100338,7 @@ async function layout(model, view) {
100176
100338
  );
100177
100339
  result = winner.layoutResult;
100178
100340
  winnerDirection = graphSpecs[winner.index].direction;
100179
- winnerOptions = graphSpecs[winner.index].options;
100341
+ winnerOptions = laidOutSpecs[winner.index].options;
100180
100342
  if (disposition === "slide") {
100181
100343
  const folded = await foldedLayout(model, view, elk);
100182
100344
  if (folded && fitScore(result) >= fitScore(folded) * 1.1) {
@@ -100190,11 +100352,10 @@ async function layout(model, view) {
100190
100352
  }
100191
100353
  } else {
100192
100354
  winnerDirection = disposition === "tall" ? "DOWN" : "RIGHT";
100193
- winnerOptions = void 0;
100194
- result = await elk.layout(makeGraph(winnerDirection));
100355
+ ({ result, options: winnerOptions } = await layoutGraph(winnerDirection));
100195
100356
  }
100196
100357
  const layoutMs = Date.now() - startTime;
100197
- let base = sceneFromResult(result, layoutMs);
100358
+ let base = sceneFromResult(result, layoutMs, winnerOptions);
100198
100359
  const layoutProfile = (candidate) => {
100199
100360
  const everyEdge = new Set(candidate.edges.map((edge) => edge.id));
100200
100361
  const profile = inspect(candidate, titleBoxesOf(candidate, model)).local(everyEdge, /* @__PURE__ */ new Map());
@@ -100204,6 +100365,8 @@ async function layout(model, view) {
100204
100365
  if (!aspectTarget && nodeCoverage(base) < DENSE_ENOUGH) {
100205
100366
  const denser = await denserLayout(base, {
100206
100367
  layout: (spec) => elk.layout(makeGraph(winnerDirection, spec)),
100368
+ // Its specs carry the hub ports (none of them sets `hubPorts: false`), and
100369
+ // a candidate elk refuses is dropped rather than retried without them.
100207
100370
  toScene: (laidOut) => sceneFromResult(laidOut, Date.now() - startTime),
100208
100371
  profile: layoutProfile
100209
100372
  });
@@ -100225,7 +100388,7 @@ async function layout(model, view) {
100225
100388
  const constrained = makeGraph(winnerDirection, winnerOptions);
100226
100389
  constrainPorts(constrained, current, flagged, model);
100227
100390
  const reresult = await elk.layout(constrained);
100228
- candidate = sceneFromResult(reresult, Date.now() - startTime);
100391
+ candidate = sceneFromResult(reresult, Date.now() - startTime, winnerOptions);
100229
100392
  } catch {
100230
100393
  break;
100231
100394
  }
@@ -100237,7 +100400,7 @@ async function layout(model, view) {
100237
100400
  }
100238
100401
  return best?.scene ?? base;
100239
100402
  }
100240
- var orderOption, semiInteractiveOption, PORT_PASS_ROUNDS, centerOf, DENSE_ENOUGH, DENSITY_GAIN, INGRESS_PARTITION, DEFAULT_INGRESS_KINDS, EGRESS_PARTITION, COMPACT_WRAP, SLOT_SCALE, SIDE_TO_ELK, ASPECT_TARGETS;
100403
+ var orderOption, semiInteractiveOption, PORT_PASS_ROUNDS, centerOf, DENSE_ENOUGH, DENSITY_GAIN, INGRESS_PARTITION, DEFAULT_INGRESS_KINDS, EGRESS_PARTITION, COMPACT_WRAP, SLOT_SCALE, SIDE_TO_ELK, sideRequest, ASPECT_TARGETS;
100241
100404
  var init_scene_layout = __esm({
100242
100405
  "src/scene-layout.ts"() {
100243
100406
  "use strict";
@@ -100268,6 +100431,7 @@ var init_scene_layout = __esm({
100268
100431
  top: "NORTH",
100269
100432
  bottom: "SOUTH"
100270
100433
  };
100434
+ sideRequest = (side) => side ? { value: side } : void 0;
100271
100435
  ASPECT_TARGETS = {
100272
100436
  slide: 16 / 9,
100273
100437
  page: 0.71
@@ -100365,6 +100529,19 @@ var init_logo_attribution = __esm({
100365
100529
  });
100366
100530
 
100367
100531
  // src/svg-render.ts
100532
+ function bandLines(text, from, right, fontSize) {
100533
+ const chars = Math.floor((right - from) / (fontSize * RENDER_CHAR_WIDTH));
100534
+ return bandWrap(text, Math.max(8, chars));
100535
+ }
100536
+ function bandWrap(text, maxChars) {
100537
+ return wrapText(text, maxChars).split("\n").flatMap((line) => {
100538
+ if (line.length <= maxChars) return [line];
100539
+ const parts = [];
100540
+ for (let index = 0; index < line.length; index += maxChars)
100541
+ parts.push(line.slice(index, index + maxChars));
100542
+ return parts;
100543
+ });
100544
+ }
100368
100545
  function assignSourceHues(model, hues) {
100369
100546
  const sourceHue = /* @__PURE__ */ new Map();
100370
100547
  for (const flow of model.flows) {
@@ -100417,19 +100594,39 @@ function auditRouteRepairs(deps) {
100417
100594
  for (let tier = 0; tier < 5; tier++) if (a[tier] !== b[tier]) return a[tier] < b[tier];
100418
100595
  return false;
100419
100596
  };
100597
+ const flowOfBreach = (key) => {
100598
+ if (key.startsWith("adrift:"))
100599
+ return labels[Number.parseInt(key.slice("adrift:".length), 10)]?.flowId ?? null;
100600
+ const body = key.slice(key.indexOf(":") + 1);
100601
+ const cut = body.search(/[~:]/);
100602
+ return cut < 0 ? body : body.slice(0, cut);
100603
+ };
100420
100604
  const withRepair = stateHarm();
100421
100605
  const breachesWith = breaches();
100422
100606
  const repairedRoutes = repaired.map((edge) => edge.pts);
100423
- for (const edge of repaired) edge.pts = edge.repairedFrom;
100424
- resettle();
100425
- const withoutRepair = stateHarm();
100426
- const breachesWithout = breaches();
100427
- const breaksAPromise = [...breachesWith].some((key) => !breachesWithout.has(key));
100428
- if (!breaksAPromise && !lessDamaged(withoutRepair, withRepair)) {
100607
+ const replacedRoutes = repaired.map((edge) => edge.repairedFrom);
100608
+ const applyKeep = (keep2) => {
100429
100609
  repaired.forEach((edge, index) => {
100430
- edge.pts = repairedRoutes[index];
100610
+ edge.pts = keep2[index] ? repairedRoutes[index] : replacedRoutes[index];
100431
100611
  });
100432
100612
  resettle();
100613
+ };
100614
+ applyKeep(repaired.map(() => false));
100615
+ const withoutRepair = stateHarm();
100616
+ const breachesWithout = breaches();
100617
+ const broken = [...breachesWith].filter((key) => !breachesWithout.has(key));
100618
+ if (!broken.length) {
100619
+ if (!lessDamaged(withoutRepair, withRepair)) applyKeep(repaired.map(() => true));
100620
+ for (const edge of repaired) edge.repairedFrom = void 0;
100621
+ return;
100622
+ }
100623
+ const blamed = new Set(broken.map(flowOfBreach).filter((id) => id !== null));
100624
+ const keep = repaired.map((edge) => !blamed.has(edge.id));
100625
+ if (keep.some(Boolean) && keep.some((kept) => !kept)) {
100626
+ applyKeep(keep);
100627
+ const partly = stateHarm();
100628
+ const stillBroken = [...breaches()].some((key) => !breachesWithout.has(key));
100629
+ if (stillBroken || lessDamaged(withoutRepair, partly)) applyKeep(repaired.map(() => false));
100433
100630
  }
100434
100631
  for (const edge of repaired) edge.repairedFrom = void 0;
100435
100632
  }
@@ -100693,15 +100890,20 @@ function createLabelSettler(deps) {
100693
100890
  const a = edge.pts[index];
100694
100891
  const b = edge.pts[index + 1];
100695
100892
  const vertical = Math.abs(a.x - b.x) < Math.abs(a.y - b.y);
100696
- const span = vertical ? Math.abs(b.y - a.y) : Math.abs(b.x - a.x);
100697
- const room = Math.max(0, (span - (vertical ? label.height : label.width)) / 2);
100698
- if (room === 0) continue;
100699
- for (const fraction of [-0.25, 0.25, -0.5, 0.5, -0.75, 0.75, -1, 1]) {
100700
- const shift = room * fraction;
100701
- seats.push(
100702
- vertical ? { x: (a.x + b.x) / 2 - label.width / 2, y: (a.y + b.y) / 2 + shift - lead } : { x: (a.x + b.x) / 2 + shift - label.width / 2, y: (a.y + b.y) / 2 - lead }
100703
- );
100704
- }
100893
+ const size = vertical ? label.height : label.width;
100894
+ const start = vertical ? Math.min(a.y, b.y) : Math.min(a.x, b.x);
100895
+ const end = vertical ? Math.max(a.y, b.y) : Math.max(a.x, b.x);
100896
+ const seat = vertical ? (a.y + b.y) / 2 - lead : (a.x + b.x) / 2 - label.width / 2;
100897
+ const back = Math.max(0, seat - start);
100898
+ const ahead = Math.max(0, end - size - seat);
100899
+ if (back === 0 && ahead === 0) continue;
100900
+ for (const fraction of [0.25, 0.5, 0.75, 1])
100901
+ for (const room of [-back, ahead]) {
100902
+ const shift = room * fraction;
100903
+ seats.push(
100904
+ vertical ? { x: (a.x + b.x) / 2 - label.width / 2, y: seat + shift } : { x: seat + shift, y: (a.y + b.y) / 2 - lead }
100905
+ );
100906
+ }
100705
100907
  }
100706
100908
  return seats;
100707
100909
  };
@@ -100748,27 +100950,103 @@ function createLabelSettler(deps) {
100748
100950
  settleLabelPositions
100749
100951
  };
100750
100952
  }
100953
+ function kindKeysSvg(paint, kinds, y, x) {
100954
+ const { scene, palette, legendNames, scaled, resolveStyle } = paint;
100955
+ let svg = "";
100956
+ let keyX = x;
100957
+ let bandY = y;
100958
+ const LINE_H = scaled(12);
100959
+ for (const kind of kinds) {
100960
+ const nodeStyle = resolveStyle(kind, "");
100961
+ if (kind === "actor") {
100962
+ const stroke = nodeStyle.stroke?.color ?? palette.actorStroke;
100963
+ svg += `<circle cx="${keyX + scaled(13)}" cy="${bandY + scaled(5)}" r="${scaled(3)}" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100964
+ `;
100965
+ svg += `<path d="M ${keyX + scaled(8)} ${bandY + scaled(15)} q ${scaled(5)} ${scaled(-7)} ${scaled(10)} 0" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100966
+ `;
100967
+ } else {
100968
+ const dash = dashArray(nodeStyle.stroke?.style);
100969
+ const stroke = nodeStyle.stroke?.color ?? palette.nodeStroke;
100970
+ svg += `<rect x="${keyX}" y="${bandY + 2}" width="${scaled(26)}" height="${scaled(14)}" rx="3" fill="${nodeStyle.fill ?? palette.nodeFill}" stroke="${escAttr(stroke)}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
100971
+ `;
100972
+ const glyphScale = (scaled(14) - scaled(4)) / GLYPH_BOX.height;
100973
+ svg += glyphSvg(kind, escAttr(stroke), {
100974
+ x: keyX + (scaled(26) - GLYPH_BOX.width * glyphScale) / 2,
100975
+ y: bandY + 2 + (scaled(14) - GLYPH_BOX.height * glyphScale) / 2,
100976
+ scale: glyphScale
100977
+ });
100978
+ }
100979
+ const name = legendNames[kind];
100980
+ const lines = bandLines(name, keyX + scaled(32), bandRightMargin(scene), scaled(10));
100981
+ for (const [row, line] of lines.entries())
100982
+ svg += `<text x="${keyX + scaled(32)}" y="${bandY + scaled(13) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
100983
+ `;
100984
+ keyX += scaled(40) + Math.ceil(name.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
100985
+ bandY += (lines.length - 1) * LINE_H;
100986
+ if (lines.length > 1 || keyX > scene.width - 220) {
100987
+ keyX = x;
100988
+ bandY += scaled(22);
100989
+ }
100990
+ }
100991
+ return { svg, bandY: kinds.length ? bandY + scaled(24) : bandY };
100992
+ }
100993
+ function lineStyleKeysSvg(paint, y, x) {
100994
+ const { scene, model, style, palette, legendLineStyles, scaled, defaultEdgeColor, markerName } = paint;
100995
+ const styles = LINE_STYLES2.filter(
100996
+ (lineStyle) => model.flows.some((flow) => lineStyleOf(flow, style) === lineStyle)
100997
+ );
100998
+ if (styles.length < 2) return { svg: "", bandY: y };
100999
+ const maxX = scene.width - 20;
101000
+ let svg = "";
101001
+ let keyX = x;
101002
+ let bandY = y;
101003
+ const LINE_H = scaled(12);
101004
+ for (const [index, lineStyle] of styles.entries()) {
101005
+ const meaning = legendLineStyles[lineStyle];
101006
+ const keyWidth = scaled(40) + Math.ceil(meaning.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
101007
+ if (keyX > x && keyX + keyWidth > maxX) {
101008
+ keyX = x;
101009
+ bandY += scaled(22);
101010
+ }
101011
+ const dash = dashArray(lineStyle);
101012
+ svg += `<line x1="${keyX}" y1="${bandY + 8}" x2="${keyX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(defaultEdgeColor)})"/>
101013
+ `;
101014
+ const lines = bandLines(meaning, keyX + scaled(40), maxX, scaled(10));
101015
+ for (const [row, line] of lines.entries())
101016
+ svg += `<text x="${keyX + scaled(32)}" y="${bandY + scaled(12) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
101017
+ `;
101018
+ if (lines.length === 1) {
101019
+ keyX += keyWidth;
101020
+ continue;
101021
+ }
101022
+ bandY += (lines.length - 1) * LINE_H;
101023
+ if (index < styles.length - 1) {
101024
+ bandY += scaled(22);
101025
+ keyX = x;
101026
+ }
101027
+ }
101028
+ return { svg, bandY: bandY + scaled(24) };
101029
+ }
100751
101030
  function createBandRenderers(paint) {
100752
101031
  const {
100753
101032
  scene,
100754
101033
  model,
100755
- view,
100756
101034
  style,
100757
101035
  palette,
100758
101036
  annot,
100759
101037
  ui,
100760
- legendNames,
100761
101038
  legendFlowLabel,
100762
101039
  scaled,
100763
101040
  objectName,
100764
- resolveStyle,
100765
101041
  defaultEdgeColor,
100766
101042
  markerName,
100767
101043
  numbered
100768
101044
  } = paint;
100769
101045
  let bandY = scene.height;
100770
101046
  let bandsSvg = "";
100771
- const contentX = 150;
101047
+ const rightMargin = bandRightMargin(scene);
101048
+ const stackedTitle = rightMargin - BAND_CONTENT_X < MIN_BAND_TEXT;
101049
+ const contentX = stackedTitle ? 20 : BAND_CONTENT_X;
100772
101050
  const chip = (x, y, name) => {
100773
101051
  const width = chipW(name, annot.scale);
100774
101052
  return {
@@ -100783,7 +101061,7 @@ function createBandRenderers(paint) {
100783
101061
  `;
100784
101062
  bandsSvg += `<text x="20" y="${bandY + scaled(32)}" font-size="${scaled(11)}" font-weight="bold" fill="${palette.bandTitle}">${esc(title)}</text>
100785
101063
  `;
100786
- bandY += scaled(20);
101064
+ bandY += scaled(stackedTitle ? 34 : 20);
100787
101065
  };
100788
101066
  const flowChipsSvg = (flow, startX, startY) => {
100789
101067
  if (!flow.objects?.length) return "";
@@ -100815,7 +101093,7 @@ function createBandRenderers(paint) {
100815
101093
  const textW = Math.max(60, colW - BADGE - (chipsW ? chipsW + 6 : 0));
100816
101094
  const maxChars = Math.max(6, Math.floor(textW / (scaled(10) * RENDER_CHAR_WIDTH)));
100817
101095
  const raw = (flow.label ?? "") + (tech ? " " + tech : "");
100818
- const lines = raw.split("\n").flatMap((segment) => wrapText(segment, maxChars).split("\n"));
101096
+ const lines = raw.split("\n").flatMap((segment) => bandWrap(segment, maxChars));
100819
101097
  return { flow, lines };
100820
101098
  });
100821
101099
  const rows = Math.ceil(entries.length / cols);
@@ -100842,66 +101120,50 @@ function createBandRenderers(paint) {
100842
101120
  };
100843
101121
  const renderObjectsBand = () => {
100844
101122
  beginBand(ui.objects);
100845
- for (const bo of model.businessObjects) {
100846
- const chipResult = chip(contentX, bandY + 2, bo.name);
101123
+ const LINE_H = scaled(12);
101124
+ const chipRow = (name, reading) => {
101125
+ const chipResult = chip(contentX, bandY + 2, name);
100847
101126
  bandsSvg += chipResult.svg;
100848
- if (bo.description)
100849
- bandsSvg += `<text x="${contentX + chipResult.width + 10}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandMuted}">\u2014 ${esc(bo.description)}</text>
101127
+ const textX = contentX + chipResult.width + 10;
101128
+ const lines = reading ? bandLines(reading, textX, rightMargin, scaled(10)) : [];
101129
+ for (const [row, line] of lines.entries())
101130
+ bandsSvg += `<text x="${textX}" y="${bandY + scaled(13) + row * LINE_H}" font-size="${scaled(10)}" fill="${palette.bandMuted}">${esc(line)}</text>
100850
101131
  `;
100851
- bandY += scaled(24);
100852
- }
100853
- const keyChip = chip(contentX, bandY + 2, ui.businessObject);
100854
- bandsSvg += keyChip.svg;
100855
- bandsSvg += `<text x="${contentX + keyChip.width + 10}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandMuted}">${esc(ui.carriedByFlow)}</text>
100856
- `;
100857
- bandY += scaled(24) + 6;
101132
+ bandY += scaled(24) + Math.max(0, lines.length - 1) * LINE_H;
101133
+ };
101134
+ for (const bo of model.businessObjects)
101135
+ chipRow(bo.name, bo.description ? `\u2014 ${bo.description}` : "");
101136
+ if (model.flows.some((flow) => flow.objects?.length))
101137
+ chipRow(ui.businessObject, ui.carriedByFlow);
101138
+ bandY += 6;
100858
101139
  };
100859
101140
  const renderLegendBand = () => {
101141
+ const kinds = legendKinds(paint);
101142
+ if (!kinds.length && !model.flows.length && !model.legendNotes.length) return;
100860
101143
  beginBand(ui.legend);
100861
- let lx = contentX;
100862
- const kindsUsed = [...new Set(scene.nodes.map((node) => node.kind))].filter(
100863
- (kind) => legendNames[kind] && (kind !== "actor" || view.actorLegend)
100864
- );
100865
- for (const kind of kindsUsed) {
100866
- const nodeStyle = resolveStyle(kind, "");
100867
- if (kind === "actor") {
100868
- const stroke = nodeStyle.stroke?.color ?? palette.actorStroke;
100869
- bandsSvg += `<circle cx="${lx + scaled(13)}" cy="${bandY + scaled(5)}" r="${scaled(3)}" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100870
- `;
100871
- bandsSvg += `<path d="M ${lx + scaled(8)} ${bandY + scaled(15)} q ${scaled(5)} ${scaled(-7)} ${scaled(10)} 0" fill="none" stroke="${stroke}" stroke-width="1.2"/>
100872
- `;
100873
- } else {
100874
- const dash = dashArray(nodeStyle.stroke?.style);
100875
- const stroke = nodeStyle.stroke?.color ?? palette.nodeStroke;
100876
- bandsSvg += `<rect x="${lx}" y="${bandY + 2}" width="${scaled(26)}" height="${scaled(14)}" rx="3" fill="${nodeStyle.fill ?? palette.nodeFill}" stroke="${escAttr(stroke)}"${dash ? ` stroke-dasharray="${dash}"` : ""}/>
101144
+ const kindKeys = kindKeysSvg(paint, kinds, bandY, contentX);
101145
+ bandsSvg += kindKeys.svg;
101146
+ bandY = kindKeys.bandY;
101147
+ if (model.flows.length) {
101148
+ bandsSvg += `<line x1="${contentX}" y1="${bandY + 8}" x2="${contentX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3" marker-end="url(#${markerName(defaultEdgeColor)})"/>
100877
101149
  `;
100878
- const glyphScale = (scaled(14) - scaled(4)) / GLYPH_BOX.height;
100879
- bandsSvg += glyphSvg(kind, escAttr(stroke), {
100880
- x: lx + (scaled(26) - GLYPH_BOX.width * glyphScale) / 2,
100881
- y: bandY + 2 + (scaled(14) - GLYPH_BOX.height * glyphScale) / 2,
100882
- scale: glyphScale
100883
- });
100884
- }
100885
- const name = legendNames[kind];
100886
- bandsSvg += `<text x="${lx + scaled(32)}" y="${bandY + scaled(13)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(name)}</text>
101150
+ const flowLabelText = (numbered ? legendFlowLabel + " \u2014 " + ui.numberedSuffix : legendFlowLabel) + (style.flowColor === "by-source" ? style.lang === "fr" ? " \u2014 couleur = source" : " \u2014 colour = source" : "");
101151
+ const flowKeyX = contentX + scaled(32);
101152
+ const flowKeyLines = bandLines(flowLabelText, flowKeyX, rightMargin, scaled(10));
101153
+ for (const [row, line] of flowKeyLines.entries())
101154
+ bandsSvg += `<text x="${flowKeyX}" y="${bandY + scaled(12) + row * scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(line)}</text>
100887
101155
  `;
100888
- lx += scaled(40) + Math.ceil(name.length * scaled(10) * RENDER_CHAR_WIDTH) + scaled(24);
100889
- if (lx > scene.width - 220) {
100890
- lx = contentX;
100891
- bandY += scaled(22);
100892
- }
101156
+ bandY += scaled(24) + (flowKeyLines.length - 1) * scaled(12);
101157
+ const lineStyleKeys = lineStyleKeysSvg(paint, bandY, contentX);
101158
+ bandsSvg += lineStyleKeys.svg;
101159
+ bandY = lineStyleKeys.bandY;
100893
101160
  }
100894
- bandY += scaled(24);
100895
- bandsSvg += `<line x1="${contentX}" y1="${bandY + 8}" x2="${contentX + scaled(26)}" y2="${bandY + 8}" stroke="${escAttr(defaultEdgeColor)}" stroke-width="1.3" marker-end="url(#${markerName(defaultEdgeColor)})"/>
100896
- `;
100897
- const flowLabelText = (numbered ? legendFlowLabel + " \u2014 " + ui.numberedSuffix : legendFlowLabel) + (style.flowColor === "by-source" ? style.lang === "fr" ? " \u2014 couleur = source" : " \u2014 colour = source" : "");
100898
- bandsSvg += `<text x="${contentX + scaled(32)}" y="${bandY + scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}">${esc(flowLabelText)}</text>
100899
- `;
100900
- bandY += scaled(24);
100901
101161
  for (const note of model.legendNotes) {
100902
- bandsSvg += `<text x="${contentX}" y="${bandY + scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}" font-style="italic">${esc(note)}</text>
101162
+ const lines = bandLines(note, contentX, rightMargin, scaled(10));
101163
+ for (const [row, line] of lines.entries())
101164
+ bandsSvg += `<text x="${contentX}" y="${bandY + scaled(12) + row * scaled(12)}" font-size="${scaled(10)}" fill="${palette.bandText}" font-style="italic">${esc(line)}</text>
100903
101165
  `;
100904
- bandY += scaled(20);
101166
+ bandY += scaled(20) + (lines.length - 1) * scaled(12);
100905
101167
  }
100906
101168
  };
100907
101169
  return {
@@ -101000,7 +101262,7 @@ function createEdgePainter(paint) {
101000
101262
  const flowStyle = flow?.style;
101001
101263
  const color = flowColorOf(flow);
101002
101264
  const headColor = style.flowColor === "by-source" ? color : defaultEdgeColor;
101003
- const dash = dashArray(flowStyle?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style);
101265
+ const dash = dashArray(lineStyleOf(flow, style));
101004
101266
  const width = flowStyle?.stroke?.width ?? style.flowStroke.width;
101005
101267
  return `<path d="${edgePath(edge.pts)}" fill="none" stroke="${escAttr(color)}" stroke-width="${width}"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#${markerName(headColor)})"/>
101006
101268
  `;
@@ -101073,6 +101335,7 @@ function render(model, view, scene, options) {
101073
101335
  const ui = UI[style.lang] ?? UI.en;
101074
101336
  const legendNames = style.lang === "fr" ? view.legendNamesFr : view.legendNames;
101075
101337
  const legendFlowLabel = style.lang === "fr" ? view.legendFlowLabelFr : view.legendFlowLabel;
101338
+ const legendLineStyles = style.lang === "fr" ? view.legendLineStylesFr : view.legendLineStyles;
101076
101339
  const elementStyle = /* @__PURE__ */ new Map();
101077
101340
  const elementLogo = /* @__PURE__ */ new Map();
101078
101341
  for (const entry of collectElementStyles(model.elements)) {
@@ -101115,6 +101378,7 @@ function render(model, view, scene, options) {
101115
101378
  });
101116
101379
  const overlapsAfter = countLabelOverlaps();
101117
101380
  compactVertical(scene);
101381
+ fitCanvas(scene);
101118
101382
  const drawnLogos = /* @__PURE__ */ new Set();
101119
101383
  const { renderContainerNode, renderLeafNode } = createNodeRenderers({
101120
101384
  palette,
@@ -101154,6 +101418,7 @@ function render(model, view, scene, options) {
101154
101418
  ui,
101155
101419
  legendNames,
101156
101420
  legendFlowLabel,
101421
+ legendLineStyles,
101157
101422
  scaled,
101158
101423
  objectName,
101159
101424
  resolveStyle,
@@ -101183,10 +101448,11 @@ function render(model, view, scene, options) {
101183
101448
  });
101184
101449
  return { svg, overlapsBefore, overlapsAfter };
101185
101450
  }
101186
- var HOP_RADIUS, LABEL_HALO, RENDER_CHAR_WIDTH, dashArray, round1, ADRIFT_SQ, ATTACHED_SQ, GLYPH_BOX, GLYPHS, LOGO_BOX;
101451
+ var HOP_RADIUS, LABEL_HALO, RENDER_CHAR_WIDTH, BAND_CONTENT_X, MIN_BAND_TEXT, bandRightMargin, dashArray, lineStyleOf, round1, ADRIFT_SQ, ATTACHED_SQ, GLYPH_BOX, GLYPHS, LOGO_BOX, legendKinds;
101187
101452
  var init_svg_render = __esm({
101188
101453
  "src/svg-render.ts"() {
101189
101454
  "use strict";
101455
+ init_views();
101190
101456
  init_themes();
101191
101457
  init_localization();
101192
101458
  init_xml_escape();
@@ -101202,7 +101468,11 @@ var init_svg_render = __esm({
101202
101468
  HOP_RADIUS = 5;
101203
101469
  LABEL_HALO = 4;
101204
101470
  RENDER_CHAR_WIDTH = 0.52;
101471
+ BAND_CONTENT_X = 150;
101472
+ MIN_BAND_TEXT = 200;
101473
+ bandRightMargin = (scene) => scene.width - 20;
101205
101474
  dashArray = (lineStyle) => lineStyle === "dashed" ? "5 3" : lineStyle === "dotted" ? "2 2.5" : void 0;
101475
+ lineStyleOf = (flow, style) => flow?.style?.stroke?.style ?? flow?.lineStyle ?? style.flowStroke.style;
101206
101476
  round1 = (n) => Math.round(n * 10) / 10;
101207
101477
  ADRIFT_SQ = 20 * 20;
101208
101478
  ATTACHED_SQ = 6 * 6;
@@ -101222,6 +101492,9 @@ var init_svg_render = __esm({
101222
101492
  device: ({ x, y, r, line }) => `<rect x="${x(2)}" y="${y(1)}" width="${r(14)}" height="${r(9)}" rx="${r(1)}" ${line}/><path d="M ${x(9)} ${y(10)} V ${y(13)}" ${line}/><path d="M ${x(4)} ${y(13)} H ${x(14)}" ${line}/>`
101223
101493
  };
101224
101494
  LOGO_BOX = { size: 18, right: 7, top: 6 };
101495
+ legendKinds = (paint) => [...new Set(paint.scene.nodes.map((node) => node.kind))].filter(
101496
+ (kind) => paint.legendNames[kind] && (kind !== "actor" || paint.view.actorLegend)
101497
+ );
101225
101498
  }
101226
101499
  });
101227
101500
 
@@ -102159,7 +102432,7 @@ CORE_I -> PARTNER : "Nightly export" (SFTP/22)
102159
102432
  };
102160
102433
  args = process.argv.slice(2);
102161
102434
  command = args[0];
102162
- version = true ? "1.0.0-RC14" : pkg.version;
102435
+ version = true ? "1.0.0-RC15" : pkg.version;
102163
102436
  VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--format", "--theme"]);
102164
102437
  positionalFile = () => {
102165
102438
  for (let index = 0; index < args.length; index++) {