@diagrammo/dgmo 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/commands/dgmo.md +17 -32
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +1 -1
  5. package/dist/advanced.cjs +791 -614
  6. package/dist/advanced.d.cts +8 -51
  7. package/dist/advanced.d.ts +8 -51
  8. package/dist/advanced.js +791 -615
  9. package/dist/auto.cjs +793 -292
  10. package/dist/auto.js +95 -95
  11. package/dist/auto.mjs +793 -292
  12. package/dist/cli.cjs +145 -328
  13. package/dist/index.cjs +792 -291
  14. package/dist/index.js +792 -291
  15. package/docs/ai-integration.md +46 -26
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +3 -3
  18. package/src/advanced.ts +0 -12
  19. package/src/boxes-and-lines/label-placement.ts +339 -0
  20. package/src/boxes-and-lines/layout-search.ts +16 -2
  21. package/src/boxes-and-lines/layout.ts +32 -7
  22. package/src/boxes-and-lines/renderer.ts +67 -139
  23. package/src/c4/parser.ts +15 -6
  24. package/src/chart.ts +1 -1
  25. package/src/class/parser.ts +6 -2
  26. package/src/cli-banner.ts +2 -3
  27. package/src/cli.ts +539 -1223
  28. package/src/cycle/parser.ts +4 -1
  29. package/src/dgmo-router.ts +16 -2
  30. package/src/diagnostics.ts +0 -0
  31. package/src/echarts.ts +5 -1
  32. package/src/er/parser.ts +5 -1
  33. package/src/gantt/parser.ts +13 -4
  34. package/src/graph/flowchart-parser.ts +5 -1
  35. package/src/graph/state-parser.ts +5 -1
  36. package/src/infra/parser.ts +10 -6
  37. package/src/journey-map/parser.ts +5 -2
  38. package/src/kanban/parser.ts +6 -2
  39. package/src/map/context-labels.ts +29 -19
  40. package/src/map/layout.ts +403 -108
  41. package/src/map/legend-band.ts +28 -0
  42. package/src/map/parser.ts +13 -4
  43. package/src/map/renderer.ts +74 -23
  44. package/src/map/resolver.ts +2 -2
  45. package/src/org/parser.ts +8 -2
  46. package/src/pyramid/parser.ts +4 -1
  47. package/src/raci/parser.ts +4 -1
  48. package/src/ring/parser.ts +4 -1
  49. package/src/sequence/parser.ts +6 -3
  50. package/src/sitemap/parser.ts +12 -3
  51. package/src/tech-radar/parser.ts +6 -3
  52. package/src/utils/parsing.ts +2 -2
  53. package/src/utils/tag-groups.ts +3 -3
  54. package/src/visualizations/parse.ts +9 -3
  55. package/src/migrate/data-comma.ts +0 -117
  56. package/src/migrate/embedded.ts +0 -238
  57. package/src/migrate/index.ts +0 -295
  58. package/src/migrate/line-classifier.ts +0 -128
  59. package/src/migrate/line-transformer.ts +0 -218
package/dist/auto.cjs CHANGED
@@ -37,6 +37,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  function makeDgmoError(line11, message, severity = "error", code) {
38
38
  return code !== void 0 ? { line: line11, message, severity, code } : { line: line11, message, severity };
39
39
  }
40
+ function dedupeDiagnostics(diagnostics) {
41
+ const seen = /* @__PURE__ */ new Set();
42
+ const out = [];
43
+ for (const d of diagnostics) {
44
+ const key = `${d.line}\0${d.column ?? ""}\0${d.severity}\0${d.code ?? ""}\0${d.message}`;
45
+ if (seen.has(key)) continue;
46
+ seen.add(key);
47
+ out.push(d);
48
+ }
49
+ return out;
50
+ }
40
51
  function formatDgmoError(err) {
41
52
  return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
42
53
  }
@@ -94,7 +105,7 @@ function vennAliasKeywordRemovedMessage(args) {
94
105
  return `Venn 'alias' keyword was removed. Use 'as' instead \u2014 '${args.name} as ${args.alias}'.`;
95
106
  }
96
107
  function pipeOperatorRemovedMessage() {
97
- return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'. Run 'dgmo migrate'`;
108
+ return `'|' removed \u2014 'Node | c: red' \u2192 'Node c: red'`;
98
109
  }
99
110
  function ganttBarePercentRemovedMessage(percent) {
100
111
  return `Bare-percent progress shorthand '| ${percent}' was removed. Use 'progress: ${percent.replace(/%$/, "")}' (integer 0\u2013100, no '%').`;
@@ -1150,7 +1161,7 @@ var init_parsing = __esm({
1150
1161
  "no-title",
1151
1162
  "no-notes"
1152
1163
  ]);
1153
- MULTIPLE_PIPE_ERROR = 'Use a single "|" to start metadata, then separate items with commas.';
1164
+ MULTIPLE_PIPE_ERROR = "'|' is no longer metadata syntax \u2014 put metadata on the same line as 'key: value, key2: value2' (no pipes), per \xA71.4.";
1154
1165
  RESERVED_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
1155
1166
  AS_ALIAS_RE = /\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})$/;
1156
1167
  POTENTIAL_META_RE = /\b([\w-]+)\s*:/;
@@ -1331,12 +1342,10 @@ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1331
1342
  );
1332
1343
  if (!isPrefix) {
1333
1344
  const defined = group.entries.map((e) => e.value);
1334
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1345
+ let msg = `Unknown value '${value}' for tag group '${group.name}' \u2014 use one of: ${defined.join(", ")}`;
1335
1346
  const hint = suggestFn?.(value, defined);
1336
1347
  if (hint) {
1337
1348
  msg += `. ${hint}`;
1338
- } else {
1339
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1340
1349
  }
1341
1350
  pushWarning(entity.lineNumber, msg);
1342
1351
  }
@@ -2112,7 +2121,10 @@ function parseVisualizationFull(content, palette) {
2112
2121
  lineNumber
2113
2122
  });
2114
2123
  } else {
2115
- warn2(lineNumber, `Unrecognized era entry: "${line11}"`);
2124
+ warn2(
2125
+ lineNumber,
2126
+ `Unrecognized era entry: "${line11}" \u2014 use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
2127
+ );
2116
2128
  }
2117
2129
  continue;
2118
2130
  }
@@ -2140,7 +2152,10 @@ function parseVisualizationFull(content, palette) {
2140
2152
  lineNumber
2141
2153
  });
2142
2154
  } else {
2143
- warn2(lineNumber, `Unrecognized marker entry: "${line11}"`);
2155
+ warn2(
2156
+ lineNumber,
2157
+ `Unrecognized marker entry: "${line11}" \u2014 use '<date> Label [color]' (e.g. '1776 Independence').`
2158
+ );
2144
2159
  }
2145
2160
  continue;
2146
2161
  }
@@ -2818,7 +2833,7 @@ function parseVisualizationFull(content, palette) {
2818
2833
  result.diagnostics.push(
2819
2834
  makeDgmoError(
2820
2835
  ov.lineNumber,
2821
- `Intersection references unknown set or alias "${ref}"`
2836
+ `Intersection references unknown set or alias "${ref}" \u2014 declare the set first on its own line ('Name [as <alias>] [color]', e.g. 'Navigation as nav blue'), then reference it by that name or alias. (\xA716.4)`
2822
2837
  )
2823
2838
  );
2824
2839
  if (!result.error)
@@ -6262,10 +6277,10 @@ function parseSequenceDgmo(content, palette) {
6262
6277
  const rawInside = fallbackMatch[1];
6263
6278
  const pipeIdx = rawInside.indexOf("|");
6264
6279
  const cleanName = rawInside.substring(0, pipeIdx).trim().replace(/\([^)]*\)$/, "").trim();
6265
- const metaPart = rawInside.substring(pipeIdx).trim();
6280
+ const metaPart = rawInside.substring(pipeIdx).replace(/^\|\s*/, "").trim();
6266
6281
  pushError(
6267
6282
  lineNumber,
6268
- `Pipe metadata must go outside brackets \u2014 use '[${cleanName}] ${metaPart}' instead of '[${rawInside.trim()}]'`
6283
+ `Metadata goes after the bracket with no pipe \u2014 use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
6269
6284
  );
6270
6285
  continue;
6271
6286
  }
@@ -6875,7 +6890,7 @@ function parseSequenceDgmo(content, palette) {
6875
6890
  }
6876
6891
  pushWarning(
6877
6892
  lineNumber,
6878
- `Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block keyword (if/loop/parallel).`
6893
+ `Unexpected line: '${trimmed}'. Expected a message ('A -> B' or 'A -label-> B'), participant ('Name' or 'Name is a actor'), note ('note right of A Text'), section ('== Name =='), or block ('if/loop/parallel <label>').`
6879
6894
  );
6880
6895
  }
6881
6896
  if (!hasExplicitChart && result.messages.length === 0) {
@@ -8037,7 +8052,11 @@ function parseFlowchart(content, palette) {
8037
8052
  const value = optMatch[2].trim();
8038
8053
  if (key === "no-color") {
8039
8054
  result.diagnostics.push(
8040
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8055
+ makeDgmoError(
8056
+ lineNumber,
8057
+ '"no-color" was removed \u2014 node colors are assigned automatically by shape; delete this line.',
8058
+ "warning"
8059
+ )
8041
8060
  );
8042
8061
  continue;
8043
8062
  }
@@ -8366,7 +8385,11 @@ function parseState(content, palette) {
8366
8385
  const value = optMatch[2].trim();
8367
8386
  if (key === "no-color") {
8368
8387
  result.diagnostics.push(
8369
- makeDgmoError(lineNumber, '"no-color" has been removed.', "warning")
8388
+ makeDgmoError(
8389
+ lineNumber,
8390
+ '"no-color" was removed \u2014 state colors are assigned automatically; delete this line.',
8391
+ "warning"
8392
+ )
8370
8393
  );
8371
8394
  continue;
8372
8395
  }
@@ -8681,7 +8704,7 @@ function parseClassDiagram(content, palette) {
8681
8704
  result.diagnostics.push(
8682
8705
  makeDgmoError(
8683
8706
  lineNumber,
8684
- '"no-auto-color" has been removed.',
8707
+ '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.',
8685
8708
  "warning"
8686
8709
  )
8687
8710
  );
@@ -8796,7 +8819,11 @@ function parseClassDiagram(content, palette) {
8796
8819
  continue;
8797
8820
  }
8798
8821
  result.diagnostics.push(
8799
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
8822
+ makeDgmoError(
8823
+ lineNumber,
8824
+ `Unexpected line: '${trimmed}'. Classes start uppercase at indent 0 ('Ship'); members are indented and colon-typed ('+ name: string', '+ sail(): void'); relationships are indented arrows ('--|> Vessel'). (\xA710)`,
8825
+ "warning"
8826
+ )
8800
8827
  );
8801
8828
  }
8802
8829
  if (notes.length > 0) {
@@ -9274,7 +9301,11 @@ function parseERDiagram(content, palette) {
9274
9301
  continue;
9275
9302
  }
9276
9303
  result.diagnostics.push(
9277
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
9304
+ makeDgmoError(
9305
+ lineNumber,
9306
+ `Unexpected line: '${trimmed}'. A table is a bare name at indent 0 ('users'); columns are indented and SPACE-separated, no colon ('id int pk'); relationships are indented ('1--* orders'). (\xA79)`,
9307
+ "warning"
9308
+ )
9278
9309
  );
9279
9310
  }
9280
9311
  if (result.tables.length === 0 && !result.error) {
@@ -9618,7 +9649,10 @@ function parseOrg(content, palette) {
9618
9649
  const value = metadataMatch[2].trim();
9619
9650
  const parent = findMetadataParent(indent, indentStack);
9620
9651
  if (!parent) {
9621
- pushError(lineNumber, "Metadata has no parent node");
9652
+ pushError(
9653
+ lineNumber,
9654
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9655
+ );
9622
9656
  } else {
9623
9657
  parent.metadata = { ...parent.metadata, [key]: value };
9624
9658
  }
@@ -9636,7 +9670,10 @@ function parseOrg(content, palette) {
9636
9670
  );
9637
9671
  attachNode(node, indent, indentStack, result);
9638
9672
  } else {
9639
- pushError(lineNumber, "Metadata has no parent node");
9673
+ pushError(
9674
+ lineNumber,
9675
+ "Metadata 'key: value' must be indented under a node \u2014 no node precedes it."
9676
+ );
9640
9677
  }
9641
9678
  } else {
9642
9679
  const node = parseNodeLabel(
@@ -9985,7 +10022,10 @@ function parseSitemap(content, palette) {
9985
10022
  if (arrowInfo) {
9986
10023
  const source = findParentNode(indent, indentStack);
9987
10024
  if (!source) {
9988
- pushError(lineNumber, "Arrow has no source node");
10025
+ pushError(
10026
+ lineNumber,
10027
+ "Arrow '-> target' must be indented under its source page \u2014 no page precedes it."
10028
+ );
9989
10029
  } else {
9990
10030
  deferredArrows.push({
9991
10031
  sourceNode: source,
@@ -10055,7 +10095,10 @@ function parseSitemap(content, palette) {
10055
10095
  const value = metadataMatch[2].trim();
10056
10096
  const parent = findParentNode(indent, indentStack);
10057
10097
  if (!parent) {
10058
- pushError(lineNumber, "Metadata has no parent node");
10098
+ pushError(
10099
+ lineNumber,
10100
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10101
+ );
10059
10102
  } else {
10060
10103
  parent.metadata = { ...parent.metadata, [key]: value };
10061
10104
  }
@@ -10075,7 +10118,10 @@ function parseSitemap(content, palette) {
10075
10118
  const key = normalizeName(node.label);
10076
10119
  labelToNode.set(key, node);
10077
10120
  } else {
10078
- pushError(lineNumber, "Metadata has no parent node");
10121
+ pushError(
10122
+ lineNumber,
10123
+ "Metadata 'key: value' must be indented under a page \u2014 no page precedes it."
10124
+ );
10079
10125
  }
10080
10126
  } else {
10081
10127
  const descResult = tryStripDescriptionKeyword(trimmed);
@@ -11893,7 +11939,7 @@ function parseChart(content, palette) {
11893
11939
  result.diagnostics.push(
11894
11940
  makeDgmoError(
11895
11941
  lineNumber,
11896
- `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`
11942
+ `'${trimmed}' removed \u2014 use a bracket container '[${trimmed.replace(/^#+\s*/, "")}]' instead (## headers are no longer supported).`
11897
11943
  )
11898
11944
  );
11899
11945
  continue;
@@ -12850,7 +12896,11 @@ function parseExtendedChartFull(content, palette) {
12850
12896
  continue;
12851
12897
  }
12852
12898
  result.diagnostics.push(
12853
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, "warning")
12899
+ makeDgmoError(
12900
+ lineNumber,
12901
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
12902
+ "warning"
12903
+ )
12854
12904
  );
12855
12905
  }
12856
12906
  const warn2 = (line11, message) => {
@@ -15212,7 +15262,10 @@ function parseKanban(content, palette) {
15212
15262
  continue;
15213
15263
  }
15214
15264
  if (!currentColumn) {
15215
- warn2(lineNumber, "Card line found before any column");
15265
+ warn2(
15266
+ lineNumber,
15267
+ "Card line found before any column. Declare a column first with '[Column Name]', then indent cards beneath it. (\xA711)"
15268
+ );
15216
15269
  continue;
15217
15270
  }
15218
15271
  if (currentCard && indent > cardBaseIndent) {
@@ -15351,7 +15404,7 @@ var init_parser13 = __esm({
15351
15404
  KNOWN_OPTIONS4 = /* @__PURE__ */ new Set(["hide", "active-tag"]);
15352
15405
  KNOWN_BOOLEANS3 = /* @__PURE__ */ new Set(["solid-fill", "no-title"]);
15353
15406
  REMOVED_BOOLEANS = {
15354
- "no-auto-color": '"no-auto-color" has been removed.'
15407
+ "no-auto-color": '"no-auto-color" was removed \u2014 colors are always auto-assigned from the palette; delete this line.'
15355
15408
  };
15356
15409
  }
15357
15410
  });
@@ -15588,7 +15641,7 @@ function parseC4(content, palette) {
15588
15641
  } else {
15589
15642
  pushError(
15590
15643
  lineNumber,
15591
- `"container ${refName}" must be inside a deployment node`
15644
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
15592
15645
  );
15593
15646
  }
15594
15647
  continue;
@@ -15643,7 +15696,10 @@ function parseC4(content, palette) {
15643
15696
  indent
15644
15697
  });
15645
15698
  } else {
15646
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
15699
+ pushError(
15700
+ lineNumber,
15701
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
15702
+ );
15647
15703
  }
15648
15704
  continue;
15649
15705
  }
@@ -15670,7 +15726,10 @@ function parseC4(content, palette) {
15670
15726
  indent
15671
15727
  });
15672
15728
  } else {
15673
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
15729
+ pushError(
15730
+ lineNumber,
15731
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
15732
+ );
15674
15733
  }
15675
15734
  continue;
15676
15735
  }
@@ -16000,7 +16059,10 @@ function parseC4(content, palette) {
16000
16059
  }
16001
16060
  desc.push(descResult.text);
16002
16061
  } else {
16003
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
16062
+ pushError(
16063
+ lineNumber,
16064
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (\xA78)`
16065
+ );
16004
16066
  }
16005
16067
  }
16006
16068
  finalizeAutoTagColors(result.tagGroups, palette);
@@ -16062,7 +16124,7 @@ function validateRelationshipTargets(result, knownNames, pushWarning) {
16062
16124
  for (const el of elements) {
16063
16125
  for (const rel of el.relationships) {
16064
16126
  if (!knownNames.has(normalizeName(rel.target))) {
16065
- let msg = `Relationship target "${rel.target}" not found`;
16127
+ let msg = `Relationship target "${rel.target}" not found \u2014 declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
16066
16128
  const hint = suggest(normalizeName(rel.target), allNames);
16067
16129
  if (hint) msg += `. ${hint}`;
16068
16130
  pushWarning(rel.lineNumber, msg, "warning");
@@ -16090,7 +16152,7 @@ function validateDeploymentRefs(result, knownNames, pushWarning) {
16090
16152
  for (const node of nodes) {
16091
16153
  for (const ref of node.containerRefs) {
16092
16154
  if (!knownNames.has(normalizeName(ref))) {
16093
- let msg = `Deployment reference "container ${ref}" not found`;
16155
+ let msg = `Deployment reference "container ${ref}" not found \u2014 declare the container element first, then reference it under the deployment node`;
16094
16156
  const hint = suggest(normalizeName(ref), allNames);
16095
16157
  if (hint) msg += `. ${hint}`;
16096
16158
  pushWarning(node.lineNumber, msg, "warning");
@@ -16809,7 +16871,7 @@ function parseInfra(content) {
16809
16871
  }
16810
16872
  warn2(
16811
16873
  lineNumber,
16812
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
16874
+ `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}' \u2014 a value is a bare word with an optional trailing color (e.g. 'Production red'); no ':' or '|'.`
16813
16875
  );
16814
16876
  continue;
16815
16877
  }
@@ -16882,7 +16944,7 @@ function parseInfra(content) {
16882
16944
  const n = deprecatedFanout[1];
16883
16945
  setError(
16884
16946
  lineNumber,
16885
- `'x${n}' fanout syntax is no longer supported \u2014 use '| fanout: ${n}' instead`
16947
+ `'x${n}' fanout syntax is no longer supported \u2014 use 'fanout: ${n}' instead`
16886
16948
  );
16887
16949
  continue;
16888
16950
  }
@@ -17056,8 +17118,10 @@ function parseInfra(content) {
17056
17118
  const hint = suggest(key, allKeys);
17057
17119
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
17058
17120
  if (hint || valueLooksNumeric) {
17059
- let msg = `Unknown property '${key}'.`;
17121
+ let msg = `Unknown infra property '${key}'.`;
17060
17122
  if (hint) msg += ` ${hint}`;
17123
+ else
17124
+ msg += ` Properties are colon-keyed 'key: value' from the known schema (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
17061
17125
  warn2(lineNumber, msg);
17062
17126
  } else if (!currentNode.isEdge) {
17063
17127
  pushDescription(currentNode, trimmed);
@@ -17068,7 +17132,7 @@ function parseInfra(content) {
17068
17132
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
17069
17133
  warn2(
17070
17134
  lineNumber,
17071
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
17135
+ `Property '${key}' belongs on the entry point \u2014 declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
17072
17136
  );
17073
17137
  }
17074
17138
  const value = parsePropertyValue(rawVal);
@@ -17175,7 +17239,7 @@ function parseInfra(content) {
17175
17239
  }
17176
17240
  warn2(
17177
17241
  lineNumber,
17178
- currentGroup ? `Unexpected line: '${trimmed}'. Expected an indented component name.` : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
17242
+ currentGroup ? `Unexpected line: '${trimmed}'. Inside a group, indent a component name under the '[Group]'.` : `Unexpected line: '${trimmed}'. Expected a node ('Api Gateway'), a group ('[Backend]'), a tag group ('tag Tier as t'), or an option ('default-rps 100'); node properties go indented under a node as 'key: value'.`
17179
17243
  );
17180
17244
  }
17181
17245
  finishCurrentNode();
@@ -17551,7 +17615,10 @@ function parseGantt(content, palette) {
17551
17615
  continue;
17552
17616
  }
17553
17617
  if (COMMENT_RE.test(line11)) continue;
17554
- warn2(lineNumber, `Unrecognized holiday entry: "${line11}"`);
17618
+ warn2(
17619
+ lineNumber,
17620
+ `Unrecognized holiday entry: "${line11}". Use a date '2024-02-19 Presidents Day' or a range '2024-05-27 -> 2024-05-29 Memorial Weekend'. (\xA713.3)`
17621
+ );
17555
17622
  continue;
17556
17623
  }
17557
17624
  }
@@ -17577,7 +17644,10 @@ function parseGantt(content, palette) {
17577
17644
  lineNumber
17578
17645
  });
17579
17646
  } else {
17580
- warn2(lineNumber, `Unrecognized era entry: "${line11}"`);
17647
+ warn2(
17648
+ lineNumber,
17649
+ `Unrecognized era entry: "${line11}". Use 'YYYY-MM-DD -> YYYY-MM-DD Label [color]' (e.g. '2026-04-06 -> 2026-04-10 Conference purple'). (\xA713.5)`
17650
+ );
17581
17651
  }
17582
17652
  continue;
17583
17653
  }
@@ -17603,7 +17673,10 @@ function parseGantt(content, palette) {
17603
17673
  lineNumber
17604
17674
  });
17605
17675
  } else {
17606
- warn2(lineNumber, `Unrecognized marker entry: "${line11}"`);
17676
+ warn2(
17677
+ lineNumber,
17678
+ `Unrecognized marker entry: "${line11}". Use 'YYYY-MM-DD Label [color]' (e.g. '2026-03-27 Board Review' or '2026-06-15 Release green'). (\xA713.6)`
17679
+ );
17607
17680
  }
17608
17681
  continue;
17609
17682
  }
@@ -17769,7 +17842,7 @@ function parseGantt(content, palette) {
17769
17842
  } else {
17770
17843
  softError(
17771
17844
  lineNumber,
17772
- `Arrow reference "${content2.name}" has no parent task context.`
17845
+ `Arrow '-> ${content2.name}' has no source task \u2014 indent it under the task it follows (e.g. 'Design 5bd' then indented '-> ${content2.name} 3bd'). (\xA713)`
17773
17846
  );
17774
17847
  }
17775
17848
  }
@@ -18989,7 +19062,10 @@ function parseMap(content, palette) {
18989
19062
  function handleTag(trimmed, line11) {
18990
19063
  const m = matchTagBlockHeading(trimmed);
18991
19064
  if (!m) {
18992
- pushError(line11, `Malformed tag declaration: "${trimmed}".`);
19065
+ pushError(
19066
+ line11,
19067
+ `Malformed tag declaration: "${trimmed}" \u2014 write 'tag <Group>' (optionally 'tag <Group> as <alias>'), e.g. 'tag Market as m', with indented 'Value color' entries below.`
19068
+ );
18993
19069
  return;
18994
19070
  }
18995
19071
  emitTagLegacyDiagnostic(m, line11, diagnostics);
@@ -19101,7 +19177,7 @@ function parseMap(content, palette) {
19101
19177
  if (!pos || pos.kind === "name" && !pos.name) {
19102
19178
  pushError(
19103
19179
  line11,
19104
- "route requires an origin: `route <origin> [style: arc]`."
19180
+ "route needs an origin place \u2014 `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (\xA724B)"
19105
19181
  );
19106
19182
  return;
19107
19183
  }
@@ -19164,7 +19240,10 @@ function parseMap(content, palette) {
19164
19240
  function handleEdges(trimmed, line11) {
19165
19241
  const parts = trimmed.split(ARROW_SPLIT);
19166
19242
  if (parts.length < 3) {
19167
- pushError(line11, `Malformed edge: "${trimmed}".`);
19243
+ pushError(
19244
+ line11,
19245
+ `Malformed edge: "${trimmed}" \u2014 an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (\xA724B)`
19246
+ );
19168
19247
  return;
19169
19248
  }
19170
19249
  const endpoints = [];
@@ -19187,7 +19266,10 @@ function parseMap(content, palette) {
19187
19266
  const from = endpoints[k];
19188
19267
  const to = endpoints[k + 1];
19189
19268
  if (!from || !to) {
19190
- pushError(line11, `Edge has an empty endpoint: "${trimmed}".`);
19269
+ pushError(
19270
+ line11,
19271
+ `Edge has an empty endpoint: "${trimmed}" \u2014 both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (\xA724B)`
19272
+ );
19191
19273
  continue;
19192
19274
  }
19193
19275
  const isLast = k === links.length - 1;
@@ -21306,7 +21388,10 @@ function parseTechRadar(content) {
21306
21388
  }
21307
21389
  if (indent === 0 && trimmed.toLowerCase() === "rings") {
21308
21390
  if (result.rings.length > 0) {
21309
- warn2(lineNumber, 'Duplicate "rings" block \u2014 using last one');
21391
+ warn2(
21392
+ lineNumber,
21393
+ 'Duplicate "rings" block \u2014 only the last is kept; merge all ring names into one "rings" block.'
21394
+ );
21310
21395
  result.rings = [];
21311
21396
  }
21312
21397
  inRingsBlock = true;
@@ -21525,7 +21610,7 @@ function parseTechRadar(content) {
21525
21610
  result.diagnostics.push(
21526
21611
  makeDgmoError(
21527
21612
  lineNumber,
21528
- `Blip "${trimmed}" has no ring assignment. Place it under a ring section header or use: ${trimmed} | ring: RingName`
21613
+ `Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring \u2014 '${trimmed} ring: RingName' (per \xA720).`
21529
21614
  )
21530
21615
  );
21531
21616
  continue;
@@ -21534,7 +21619,7 @@ function parseTechRadar(content) {
21534
21619
  if (!trimmed.includes("|") && headerParsed && result.rings.length > 0) {
21535
21620
  warn2(
21536
21621
  lineNumber,
21537
- `Unrecognized line "${trimmed}". Quadrant headers require pipe metadata: ${trimmed} | quadrant: top-left`
21622
+ `Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per \xA720).`
21538
21623
  );
21539
21624
  }
21540
21625
  }
@@ -21827,7 +21912,10 @@ function parseCycle(content) {
21827
21912
  currentNode.description.push(descLine);
21828
21913
  continue;
21829
21914
  }
21830
- warn2(lineNum, `Unexpected indented line: "${trimmed}".`);
21915
+ warn2(
21916
+ lineNum,
21917
+ `Indented line "${trimmed}" has no step above it \u2014 add a step on a non-indented line first, then indent its details. (\xA721)`
21918
+ );
21831
21919
  continue;
21832
21920
  }
21833
21921
  }
@@ -22130,7 +22218,7 @@ function parseJourneyMap(content, palette) {
22130
22218
  if (trimmed.includes("|") || !PHASE_RE.test(trimmed)) {
22131
22219
  warn2(
22132
22220
  lineNumber,
22133
- "Steps outside phases will be ignored when phases are present"
22221
+ `Step "${trimmed}" sits outside any phase and is ignored \u2014 indent it under a '[Phase]' header (per \xA722).`
22134
22222
  );
22135
22223
  continue;
22136
22224
  }
@@ -22187,7 +22275,10 @@ function parseJourneyMap(content, palette) {
22187
22275
  continue;
22188
22276
  }
22189
22277
  if (indent > 0 && !currentPhase && hasPhases) {
22190
- warn2(lineNumber, `Unexpected indented line outside of a phase`);
22278
+ warn2(
22279
+ lineNumber,
22280
+ `Indented line outside any phase \u2014 add a '[Phase]' header above it, or unindent it to a top-level step. (\xA722)`
22281
+ );
22191
22282
  }
22192
22283
  }
22193
22284
  if (currentStep) {
@@ -22533,7 +22624,10 @@ function parsePyramid(content) {
22533
22624
  continue;
22534
22625
  }
22535
22626
  if (!currentLayer) {
22536
- warn2(lineNum, `Unexpected indented line: "${trimmed}".`);
22627
+ warn2(
22628
+ lineNum,
22629
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA723)`
22630
+ );
22537
22631
  continue;
22538
22632
  }
22539
22633
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -22707,7 +22801,10 @@ function parseRing(content) {
22707
22801
  continue;
22708
22802
  }
22709
22803
  if (!currentLayer) {
22710
- warn2(lineNum, `Unexpected indented line: "${trimmed}".`);
22804
+ warn2(
22805
+ lineNum,
22806
+ `Indented description "${trimmed}" has no layer above it \u2014 add a layer name on a non-indented line first. (\xA724)`
22807
+ );
22711
22808
  continue;
22712
22809
  }
22713
22810
  const descLine = trimmed.startsWith("- ") ? `\u2022 ${trimmed.substring(2)}` : trimmed;
@@ -23433,7 +23530,10 @@ function parseRaci(content, palette) {
23433
23530
  continue;
23434
23531
  }
23435
23532
  if (bodyStarted) {
23436
- errorAt(lineNumber, `Unexpected line: '${trimmed}'.`);
23533
+ errorAt(
23534
+ lineNumber,
23535
+ `Unexpected line: '${trimmed}'. In the body expect a '[Phase]' header, an indented task name, or an assignment 'Role: A R C I' under a task. (\xA724A)`
23536
+ );
23437
23537
  } else {
23438
23538
  warn2(lineNumber, `Unrecognized directive: '${trimmed}'.`);
23439
23539
  }
@@ -23980,6 +24080,13 @@ function getAllChartTypes() {
23980
24080
  return chartTypes.map((c) => c.id);
23981
24081
  }
23982
24082
  function parseDgmo(content) {
24083
+ const result = parseDgmoUndeduped(content);
24084
+ return {
24085
+ diagnostics: dedupeDiagnostics(result.diagnostics),
24086
+ chartType: result.chartType
24087
+ };
24088
+ }
24089
+ function parseDgmoUndeduped(content) {
23983
24090
  const chartType = parseDgmoChartType(content);
23984
24091
  if (!chartType) {
23985
24092
  const colonDiag = detectColonChartType(content);
@@ -29044,63 +29151,6 @@ function ensureArrowMarkers(defs, colors) {
29044
29151
  ).attr("fill", color);
29045
29152
  }
29046
29153
  }
29047
- function edgePolylineMidpoint(points) {
29048
- if (points.length === 0) return { x: 0, y: 0 };
29049
- if (points.length === 1) return { x: points[0].x, y: points[0].y };
29050
- let total = 0;
29051
- const segLen = [];
29052
- for (let k = 1; k < points.length; k++) {
29053
- const len = Math.hypot(
29054
- points[k].x - points[k - 1].x,
29055
- points[k].y - points[k - 1].y
29056
- );
29057
- segLen.push(len);
29058
- total += len;
29059
- }
29060
- let half = total / 2;
29061
- for (let k = 1; k < points.length; k++) {
29062
- const len = segLen[k - 1];
29063
- if (half <= len || k === points.length - 1) {
29064
- const t = len === 0 ? 0 : Math.min(1, half / len);
29065
- return {
29066
- x: points[k - 1].x + (points[k].x - points[k - 1].x) * t,
29067
- y: points[k - 1].y + (points[k].y - points[k - 1].y) * t
29068
- };
29069
- }
29070
- half -= len;
29071
- }
29072
- const last = points[points.length - 1];
29073
- return { x: last.x, y: last.y };
29074
- }
29075
- function resolveEdgeLabelOverlaps(labels) {
29076
- const MAX_PASSES = 8;
29077
- const PAD = 4;
29078
- for (let pass = 0; pass < MAX_PASSES; pass++) {
29079
- let moved = false;
29080
- for (let i = 0; i < labels.length; i++) {
29081
- for (let j = i + 1; j < labels.length; j++) {
29082
- const a = labels[i];
29083
- const b = labels[j];
29084
- const dx = Math.abs(a.x - b.x);
29085
- const dy = Math.abs(a.y - b.y);
29086
- const overlapX = (a.width + b.width) / 2 + PAD - dx;
29087
- const overlapY = (a.height + b.height) / 2 + PAD - dy;
29088
- if (overlapX > 0 && overlapY > 0) {
29089
- const shift = overlapY / 2 + 1;
29090
- if (a.y < b.y) {
29091
- a.y -= shift;
29092
- b.y += shift;
29093
- } else {
29094
- a.y += shift;
29095
- b.y -= shift;
29096
- }
29097
- moved = true;
29098
- }
29099
- }
29100
- }
29101
- if (!moved) break;
29102
- }
29103
- }
29104
29154
  function renderBoxesAndLines(container, parsed, layout, palette, isDark, options) {
29105
29155
  const {
29106
29156
  onClickItem,
@@ -29307,8 +29357,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29307
29357
  groupG.append("text").attr("class", "bl-group-label").attr("x", gx + group.width / 2).attr("y", gy + 18).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sGroupLabelFontSize).attr("font-weight", "600").attr("fill", palette.text).text(group.label);
29308
29358
  }
29309
29359
  }
29310
- const labelPositions = [];
29311
- const edgeGroups = /* @__PURE__ */ new Map();
29312
29360
  for (let i = 0; i < layout.edges.length; i++) {
29313
29361
  const le = layout.edges[i];
29314
29362
  const color = edgeColorMap.get(i) ?? palette.textMuted;
@@ -29327,7 +29375,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29327
29375
  const nodeLayout = layoutNodeMap.get(le.source);
29328
29376
  if (nodeLayout) {
29329
29377
  const edgeG2 = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
29330
- edgeGroups.set(i, edgeG2);
29331
29378
  const markerId2 = `bl-arrow-${color.replace("#", "")}`;
29332
29379
  const cx = nodeLayout.x;
29333
29380
  const cy = nodeLayout.y;
@@ -29376,7 +29423,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29376
29423
  }
29377
29424
  if (points.length < 2) continue;
29378
29425
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
29379
- edgeGroups.set(i, edgeG);
29380
29426
  const markerId = `bl-arrow-${color.replace("#", "")}`;
29381
29427
  const gen = le.straight ? lineGeneratorStraight : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
29382
29428
  const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", sEdgeStrokeWidth).attr("marker-end", `url(#${markerId})`);
@@ -29384,31 +29430,6 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29384
29430
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
29385
29431
  path.attr("marker-start", `url(#${revId})`);
29386
29432
  }
29387
- if (le.label && le.points.length > 0) {
29388
- const lw = measureText(le.label, sEdgeLabelFontSize);
29389
- const labelH = sEdgeLabelFontSize + 6;
29390
- const mid = edgePolylineMidpoint(le.points);
29391
- let ly = mid.y;
29392
- if (le.parallelCount > 1 && le.yOffset !== 0) {
29393
- ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
29394
- }
29395
- labelPositions.push({
29396
- x: mid.x,
29397
- y: ly,
29398
- width: lw + 8,
29399
- height: labelH,
29400
- idx: i
29401
- });
29402
- }
29403
- }
29404
- resolveEdgeLabelOverlaps(labelPositions);
29405
- for (const lp of labelPositions) {
29406
- const le = layout.edges[lp.idx];
29407
- if (!le.label) continue;
29408
- const edgeG = edgeGroups.get(lp.idx);
29409
- const target = edgeG ?? diagramG;
29410
- target.append("rect").attr("x", lp.x - lp.width / 2).attr("y", lp.y - lp.height / 2).attr("width", lp.width).attr("height", lp.height).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
29411
- target.append("text").attr("x", lp.x).attr("y", lp.y + sEdgeLabelFontSize / 3).attr("text-anchor", "middle").attr("font-size", sEdgeLabelFontSize).attr("fill", palette.textMuted).text(le.label);
29412
29433
  }
29413
29434
  for (const ln of layout.nodes) {
29414
29435
  const node = nodeMap.get(ln.label);
@@ -29459,8 +29480,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29459
29480
  nodeG.append("rect").attr("x", x).attr("y", y).attr("width", ln.width).attr("height", ln.height).attr("rx", NODE_RX).attr("ry", NODE_RX).attr("fill", colors.fill).attr("stroke", colors.stroke).attr("stroke-width", sNodeStrokeWidth);
29460
29481
  const desc = node.description;
29461
29482
  if (desc && desc.length > 0 && !hideDescriptions) {
29462
- const MAX_LABEL_LINES3 = 3;
29463
- const fitted = fitLabelToHeader(node.label, ln.width, MAX_LABEL_LINES3);
29483
+ const MAX_LABEL_LINES4 = 3;
29484
+ const fitted = fitLabelToHeader(node.label, ln.width, MAX_LABEL_LINES4);
29464
29485
  const labelLines = fitted.lines;
29465
29486
  const labelLineH = fitted.fontSize * 1.3;
29466
29487
  const labelTotalH = labelLines.length * labelLineH;
@@ -29594,6 +29615,35 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
29594
29615
  }
29595
29616
  }
29596
29617
  }
29618
+ const labelLayer = diagramG.append("g").attr("class", "bl-edge-labels");
29619
+ const labelLineHeight = sEdgeLabelFontSize * 1.3;
29620
+ for (const le of layout.edges) {
29621
+ if (!le.labelLines || le.labelLines.length === 0) continue;
29622
+ if (le.labelX === void 0 || le.labelY === void 0) continue;
29623
+ const lx = le.labelX;
29624
+ const ly = le.labelY;
29625
+ if (hidden.size > 0) {
29626
+ let isHidden = false;
29627
+ for (const [groupKey, hiddenVals] of hidden) {
29628
+ const val = le.metadata[groupKey];
29629
+ if (val && hiddenVals.has(val.toLowerCase())) {
29630
+ isHidden = true;
29631
+ break;
29632
+ }
29633
+ }
29634
+ if (isHidden) continue;
29635
+ }
29636
+ const lw = le.labelWidth ?? 0;
29637
+ const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
29638
+ const lines = le.labelLines;
29639
+ const labelG = labelLayer.append("g").attr("class", "bl-edge-label").attr("data-line-number", String(le.lineNumber));
29640
+ labelG.append("rect").attr("x", lx - lw / 2).attr("y", ly - lh / 2).attr("width", lw).attr("height", lh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.72);
29641
+ const text = labelG.append("text").attr("x", lx).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sEdgeLabelFontSize).attr("fill", palette.textMuted);
29642
+ const firstY = ly - (lines.length - 1) / 2 * labelLineHeight + sEdgeLabelFontSize / 3;
29643
+ lines.forEach((line11, k) => {
29644
+ text.append("tspan").attr("x", lx).attr("y", firstY + k * labelLineHeight).text(line11);
29645
+ });
29646
+ }
29597
29647
  const hasDescriptions = parsed.nodes.some(
29598
29648
  (n) => n.description && n.description.length > 0
29599
29649
  );
@@ -29710,6 +29760,222 @@ var init_renderer6 = __esm({
29710
29760
  }
29711
29761
  });
29712
29762
 
29763
+ // src/boxes-and-lines/label-placement.ts
29764
+ function measureEdgeLabel(label, fontSize = EDGE_LABEL_FONT_SIZE6, maxWidth = LABEL_MAX_WIDTH) {
29765
+ let lines = wrapTextToWidth(label, fontSize, maxWidth, { hardBreak: true });
29766
+ if (lines.length > MAX_LABEL_LINES) {
29767
+ const kept = lines.slice(0, MAX_LABEL_LINES - 1);
29768
+ const rest = lines.slice(MAX_LABEL_LINES - 1).join(" ");
29769
+ kept.push(truncateText(rest, fontSize, maxWidth));
29770
+ lines = kept;
29771
+ }
29772
+ let textW = 0;
29773
+ for (const ln of lines) {
29774
+ const w = measureText(ln, fontSize);
29775
+ if (w > textW) textW = w;
29776
+ }
29777
+ return {
29778
+ lines,
29779
+ width: textW + 2 * H_PAD,
29780
+ height: lines.length * fontSize * LABEL_LINE_HEIGHT + 2 * V_PAD
29781
+ };
29782
+ }
29783
+ function edgePolylineMidpoint(points) {
29784
+ return pointAtArcFraction(points, 0.5).p;
29785
+ }
29786
+ function pointAtArcFraction(points, t) {
29787
+ if (points.length === 0) return { p: { x: 0, y: 0 }, nx: 0, ny: 1 };
29788
+ if (points.length === 1)
29789
+ return { p: { x: points[0].x, y: points[0].y }, nx: 0, ny: 1 };
29790
+ const segLen = [];
29791
+ let total = 0;
29792
+ for (let k = 1; k < points.length; k++) {
29793
+ const len = Math.hypot(
29794
+ points[k].x - points[k - 1].x,
29795
+ points[k].y - points[k - 1].y
29796
+ );
29797
+ segLen.push(len);
29798
+ total += len;
29799
+ }
29800
+ let want = total * Math.max(0, Math.min(1, t));
29801
+ for (let k = 1; k < points.length; k++) {
29802
+ const len = segLen[k - 1];
29803
+ if (want <= len || k === points.length - 1) {
29804
+ const f = len === 0 ? 0 : Math.min(1, want / len);
29805
+ const ax = points[k - 1].x;
29806
+ const ay = points[k - 1].y;
29807
+ const bx = points[k].x;
29808
+ const by = points[k].y;
29809
+ const dx = bx - ax;
29810
+ const dy = by - ay;
29811
+ const dlen = Math.hypot(dx, dy) || 1;
29812
+ return {
29813
+ p: { x: ax + dx * f, y: ay + dy * f },
29814
+ nx: -dy / dlen,
29815
+ ny: dx / dlen
29816
+ };
29817
+ }
29818
+ want -= len;
29819
+ }
29820
+ const last = points[points.length - 1];
29821
+ return { p: { x: last.x, y: last.y }, nx: 0, ny: 1 };
29822
+ }
29823
+ function rectFromCenter(cx, cy, w, h) {
29824
+ return {
29825
+ minX: cx - w / 2,
29826
+ minY: cy - h / 2,
29827
+ maxX: cx + w / 2,
29828
+ maxY: cy + h / 2
29829
+ };
29830
+ }
29831
+ function overlapsAny(cx, cy, w, h, obstacles, pad2) {
29832
+ const minX = cx - w / 2 - pad2;
29833
+ const minY = cy - h / 2 - pad2;
29834
+ const maxX = cx + w / 2 + pad2;
29835
+ const maxY = cy + h / 2 + pad2;
29836
+ for (const o of obstacles) {
29837
+ if (minX < o.maxX && maxX > o.minX && minY < o.maxY && maxY > o.minY)
29838
+ return true;
29839
+ }
29840
+ return false;
29841
+ }
29842
+ function slideFractions() {
29843
+ const out = [0.5];
29844
+ for (let k = 1; k <= SLIDE_SAMPLES; k++) {
29845
+ const off = k * 0.08;
29846
+ if (0.5 - off >= 0.12) out.push(0.5 - off);
29847
+ if (0.5 + off <= 0.88) out.push(0.5 + off);
29848
+ }
29849
+ return out;
29850
+ }
29851
+ function findClearPosition(w, h, points, obstacles) {
29852
+ const ts = slideFractions();
29853
+ for (const t of ts) {
29854
+ const { p } = pointAtArcFraction(points, t);
29855
+ if (!overlapsAny(p.x, p.y, w, h, obstacles, BOX_CLEAR_PAD)) return p;
29856
+ }
29857
+ for (let mag = PERP_STEP; mag <= PERP_MAX; mag += PERP_STEP) {
29858
+ for (const t of ts) {
29859
+ const { p, nx, ny } = pointAtArcFraction(points, t);
29860
+ for (const sign of [-1, 1]) {
29861
+ const x = p.x + nx * mag * sign;
29862
+ const y = p.y + ny * mag * sign;
29863
+ if (!overlapsAny(x, y, w, h, obstacles, BOX_CLEAR_PAD)) return { x, y };
29864
+ }
29865
+ }
29866
+ }
29867
+ return null;
29868
+ }
29869
+ function resolveLabelOverlaps(boxes) {
29870
+ const MAX_PASSES = 8;
29871
+ const PAD = 4;
29872
+ for (let pass = 0; pass < MAX_PASSES; pass++) {
29873
+ let moved = false;
29874
+ for (let i = 0; i < boxes.length; i++) {
29875
+ for (let j = i + 1; j < boxes.length; j++) {
29876
+ const a = boxes[i];
29877
+ const b = boxes[j];
29878
+ const dx = Math.abs(a.cx - b.cx);
29879
+ const dy = Math.abs(a.cy - b.cy);
29880
+ const overlapX = (a.w + b.w) / 2 + PAD - dx;
29881
+ const overlapY = (a.h + b.h) / 2 + PAD - dy;
29882
+ if (overlapX > 0 && overlapY > 0) {
29883
+ const shift = overlapY / 2 + 1;
29884
+ if (a.cy < b.cy) {
29885
+ a.cy -= shift;
29886
+ b.cy += shift;
29887
+ } else {
29888
+ a.cy += shift;
29889
+ b.cy -= shift;
29890
+ }
29891
+ moved = true;
29892
+ }
29893
+ }
29894
+ }
29895
+ if (!moved) break;
29896
+ }
29897
+ }
29898
+ function placeEdgeLabels(layout, opts) {
29899
+ const fontSize = opts?.fontSize ?? EDGE_LABEL_FONT_SIZE6;
29900
+ const maxWidth = opts?.maxWidth ?? LABEL_MAX_WIDTH;
29901
+ const obstacles = [];
29902
+ for (const n of layout.nodes)
29903
+ obstacles.push(rectFromCenter(n.x, n.y, n.width, n.height));
29904
+ for (const g of layout.groups)
29905
+ if (g.collapsed)
29906
+ obstacles.push(rectFromCenter(g.x, g.y, g.width, g.height));
29907
+ const boxes = [];
29908
+ layout.edges.forEach((e, idx) => {
29909
+ if (!e.label || e.points.length < 2) return;
29910
+ const m = measureEdgeLabel(e.label, fontSize, maxWidth);
29911
+ const mid = edgePolylineMidpoint(e.points);
29912
+ let cy = mid.y;
29913
+ if (e.parallelCount > 1 && e.yOffset !== 0)
29914
+ cy += (e.yOffset < 0 ? -1 : 1) * (m.height / 2);
29915
+ boxes.push({
29916
+ edgeIdx: idx,
29917
+ cx: mid.x,
29918
+ cy,
29919
+ w: m.width,
29920
+ h: m.height,
29921
+ lines: m.lines,
29922
+ resolved: true
29923
+ });
29924
+ });
29925
+ for (const box of boxes) {
29926
+ if (!overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
29927
+ continue;
29928
+ const e = layout.edges[box.edgeIdx];
29929
+ const clear = findClearPosition(box.w, box.h, e.points, obstacles);
29930
+ if (clear) {
29931
+ box.cx = clear.x;
29932
+ box.cy = clear.y;
29933
+ } else {
29934
+ box.resolved = false;
29935
+ }
29936
+ }
29937
+ resolveLabelOverlaps(boxes);
29938
+ for (const box of boxes) {
29939
+ if (overlapsAny(box.cx, box.cy, box.w, box.h, obstacles, BOX_CLEAR_PAD))
29940
+ box.resolved = false;
29941
+ }
29942
+ const boxByIdx = /* @__PURE__ */ new Map();
29943
+ for (const b of boxes) boxByIdx.set(b.edgeIdx, b);
29944
+ const edges = layout.edges.map((e, idx) => {
29945
+ const b = boxByIdx.get(idx);
29946
+ if (!b) return e;
29947
+ return {
29948
+ ...e,
29949
+ labelX: b.cx,
29950
+ labelY: b.cy,
29951
+ labelWidth: b.w,
29952
+ labelHeight: b.h,
29953
+ labelLines: b.lines
29954
+ };
29955
+ });
29956
+ return {
29957
+ layout: { ...layout, edges },
29958
+ unresolved: boxes.filter((b) => !b.resolved).map((b) => b.edgeIdx)
29959
+ };
29960
+ }
29961
+ var EDGE_LABEL_FONT_SIZE6, LABEL_MAX_WIDTH, MAX_LABEL_LINES, LABEL_LINE_HEIGHT, H_PAD, V_PAD, BOX_CLEAR_PAD, PERP_STEP, PERP_MAX, SLIDE_SAMPLES;
29962
+ var init_label_placement = __esm({
29963
+ "src/boxes-and-lines/label-placement.ts"() {
29964
+ "use strict";
29965
+ init_text_measure();
29966
+ EDGE_LABEL_FONT_SIZE6 = 11;
29967
+ LABEL_MAX_WIDTH = 160;
29968
+ MAX_LABEL_LINES = 3;
29969
+ LABEL_LINE_HEIGHT = 1.3;
29970
+ H_PAD = 6;
29971
+ V_PAD = 3;
29972
+ BOX_CLEAR_PAD = 4;
29973
+ PERP_STEP = 8;
29974
+ PERP_MAX = 40;
29975
+ SLIDE_SAMPLES = 9;
29976
+ }
29977
+ });
29978
+
29713
29979
  // src/boxes-and-lines/layout-layered.ts
29714
29980
  function rng(s) {
29715
29981
  return () => {
@@ -31497,6 +31763,7 @@ function edgeLength(layout) {
31497
31763
  }
31498
31764
  async function layoutBoxesAndLinesSearch(parsed, collapseInfo, opts) {
31499
31765
  const hideDescriptions = opts?.hideDescriptions ?? false;
31766
+ const reserveEdgeLabels = opts?.reserveEdgeLabels ?? false;
31500
31767
  const onProgress = opts?.onProgress;
31501
31768
  const tick = onProgress ? () => new Promise((r) => setTimeout(r)) : () => void 0;
31502
31769
  const collapsedGroupLabels = /* @__PURE__ */ new Set();
@@ -31789,8 +32056,14 @@ async function layoutBoxesAndLinesSearch(parsed, collapseInfo, opts) {
31789
32056
  for (const e of ord(parsed.edges)) {
31790
32057
  const s = srcRep(e.source);
31791
32058
  const t = tgtRep(e.target);
31792
- if (s !== t && g.hasNode(s) && g.hasNode(t))
31793
- g.setEdge(s, t, {}, edgeKey(e));
32059
+ if (s !== t && g.hasNode(s) && g.hasNode(t)) {
32060
+ let edgeLabel = {};
32061
+ if (reserveEdgeLabels && e.label) {
32062
+ const m = measureEdgeLabel(e.label, EDGE_LABEL_FONT_SIZE6);
32063
+ edgeLabel = { width: m.width, height: m.height, labelpos: "c" };
32064
+ }
32065
+ g.setEdge(s, t, edgeLabel, edgeKey(e));
32066
+ }
31794
32067
  }
31795
32068
  import_dagre4.default.layout(g);
31796
32069
  const nodes = parsed.nodes.map((n2) => {
@@ -32021,6 +32294,7 @@ var init_layout_search = __esm({
32021
32294
  init_layout5();
32022
32295
  init_layout_layered();
32023
32296
  init_layout_grouped();
32297
+ init_label_placement();
32024
32298
  DEFAULT_LAMBDA = 4;
32025
32299
  ESCALATE_THRESHOLD = 4;
32026
32300
  ESCALATE_MAX_N = 45;
@@ -32079,9 +32353,9 @@ function estimateLabelLines(label, nodeWidth = NODE_WIDTH) {
32079
32353
  current = word;
32080
32354
  }
32081
32355
  }
32082
- if (lines <= MAX_LABEL_LINES) return Math.min(lines, MAX_LABEL_LINES);
32356
+ if (lines <= MAX_LABEL_LINES2) return Math.min(lines, MAX_LABEL_LINES2);
32083
32357
  }
32084
- return MAX_LABEL_LINES;
32358
+ return MAX_LABEL_LINES2;
32085
32359
  }
32086
32360
  function computeNodeSize(node, reserveValueRow) {
32087
32361
  if (!node.description || node.description.length === 0) {
@@ -32089,7 +32363,7 @@ function computeNodeSize(node, reserveValueRow) {
32089
32363
  }
32090
32364
  const w = DESC_NODE_WIDTH;
32091
32365
  const labelLines = estimateLabelLines(node.label, w);
32092
- const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT + LABEL_PAD;
32366
+ const labelHeight = labelLines * 13 * LABEL_LINE_HEIGHT2 + LABEL_PAD;
32093
32367
  const maxTextWidth = w - 24;
32094
32368
  let totalRenderedLines = 0;
32095
32369
  for (const line11 of node.description) {
@@ -32113,7 +32387,7 @@ function computeNodeSize(node, reserveValueRow) {
32113
32387
  }
32114
32388
  async function layoutBoxesAndLines(parsed, collapseInfo, layoutOptions) {
32115
32389
  const { layoutBoxesAndLinesSearch: layoutBoxesAndLinesSearch2 } = await Promise.resolve().then(() => (init_layout_search(), layout_search_exports));
32116
- const searched = await layoutBoxesAndLinesSearch2(parsed, collapseInfo, {
32390
+ const searchOpts = {
32117
32391
  ...layoutOptions?.hideDescriptions !== void 0 && {
32118
32392
  hideDescriptions: layoutOptions.hideDescriptions
32119
32393
  },
@@ -32123,12 +32397,23 @@ async function layoutBoxesAndLines(parsed, collapseInfo, layoutOptions) {
32123
32397
  ...layoutOptions?.onProgress !== void 0 && {
32124
32398
  onProgress: layoutOptions.onProgress
32125
32399
  }
32126
- });
32127
- return attachNotes(
32128
- applyParallelEdgeOffsets(searched),
32400
+ };
32401
+ const searched = await layoutBoxesAndLinesSearch2(
32129
32402
  parsed,
32130
- layoutOptions?.collapsedNotes
32131
- );
32403
+ collapseInfo,
32404
+ searchOpts
32405
+ );
32406
+ let placed = placeEdgeLabels(applyParallelEdgeOffsets(searched));
32407
+ if (placed.unresolved.length > 0) {
32408
+ const relaid = await layoutBoxesAndLinesSearch2(parsed, collapseInfo, {
32409
+ ...searchOpts,
32410
+ reserveEdgeLabels: true
32411
+ });
32412
+ const relaidPlaced = placeEdgeLabels(applyParallelEdgeOffsets(relaid));
32413
+ if (relaidPlaced.unresolved.length < placed.unresolved.length)
32414
+ placed = relaidPlaced;
32415
+ }
32416
+ return attachNotes(placed.layout, parsed, layoutOptions?.collapsedNotes);
32132
32417
  }
32133
32418
  function attachNotes(layout, parsed, collapsedNotes) {
32134
32419
  const notesSuppressed = parsed.options?.["no-notes"] === "on";
@@ -32196,6 +32481,7 @@ function attachNotes(layout, parsed, collapsedNotes) {
32196
32481
  points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
32197
32482
  ...e.labelX !== void 0 && { labelX: e.labelX + shiftX },
32198
32483
  ...e.labelY !== void 0 && { labelY: e.labelY + shiftY }
32484
+ // labelWidth/labelHeight/labelLines are shift-invariant — carried via spread.
32199
32485
  })) : layout.edges;
32200
32486
  const finalGroups = shifted ? layout.groups.map((grp) => ({
32201
32487
  ...grp,
@@ -32240,11 +32526,12 @@ function applyParallelEdgeOffsets(layout) {
32240
32526
  }))
32241
32527
  };
32242
32528
  }
32243
- var MARGIN5, MAX_PARALLEL_EDGES, PARALLEL_SPACING, PHI, NODE_HEIGHT, NODE_WIDTH, DESC_NODE_WIDTH, DESC_FONT_SIZE2, DESC_LINE_HEIGHT2, DESC_PADDING, SEPARATOR_GAP4, MAX_DESC_LINES2, MAX_LABEL_LINES, LABEL_LINE_HEIGHT, LABEL_PAD, VALUE_ROW_FONT, VALUE_ROW_H;
32529
+ var MARGIN5, MAX_PARALLEL_EDGES, PARALLEL_SPACING, PHI, NODE_HEIGHT, NODE_WIDTH, DESC_NODE_WIDTH, DESC_FONT_SIZE2, DESC_LINE_HEIGHT2, DESC_PADDING, SEPARATOR_GAP4, MAX_DESC_LINES2, MAX_LABEL_LINES2, LABEL_LINE_HEIGHT2, LABEL_PAD, VALUE_ROW_FONT, VALUE_ROW_H;
32244
32530
  var init_layout5 = __esm({
32245
32531
  "src/boxes-and-lines/layout.ts"() {
32246
32532
  "use strict";
32247
32533
  init_text_measure();
32534
+ init_label_placement();
32248
32535
  init_notes();
32249
32536
  MARGIN5 = 40;
32250
32537
  MAX_PARALLEL_EDGES = 5;
@@ -32258,8 +32545,8 @@ var init_layout5 = __esm({
32258
32545
  DESC_PADDING = 8;
32259
32546
  SEPARATOR_GAP4 = 4;
32260
32547
  MAX_DESC_LINES2 = 6;
32261
- MAX_LABEL_LINES = 3;
32262
- LABEL_LINE_HEIGHT = 1.3;
32548
+ MAX_LABEL_LINES2 = 3;
32549
+ LABEL_LINE_HEIGHT2 = 1.3;
32263
32550
  LABEL_PAD = 12;
32264
32551
  VALUE_ROW_FONT = 11;
32265
32552
  VALUE_ROW_H = SEPARATOR_GAP4 + VALUE_ROW_FONT * DESC_LINE_HEIGHT2 + DESC_PADDING;
@@ -32278,7 +32565,7 @@ function tokenize(text) {
32278
32565
  return tokens;
32279
32566
  }
32280
32567
  function tryWrap(tokens, maxWidth, fontSize, maxLines) {
32281
- const availWidth = maxWidth - H_PAD;
32568
+ const availWidth = maxWidth - H_PAD2;
32282
32569
  const lines = [];
32283
32570
  let currentLine = "";
32284
32571
  for (const token of tokens) {
@@ -32303,7 +32590,7 @@ function tryWrap(tokens, maxWidth, fontSize, maxLines) {
32303
32590
  return lines;
32304
32591
  }
32305
32592
  function truncateLastLine(lines, maxWidth, fontSize) {
32306
- const availWidth = maxWidth - H_PAD;
32593
+ const availWidth = maxWidth - H_PAD2;
32307
32594
  const result = [...lines];
32308
32595
  const last = result[result.length - 1];
32309
32596
  if (measureText(last, fontSize) > availWidth) {
@@ -32311,7 +32598,7 @@ function truncateLastLine(lines, maxWidth, fontSize) {
32311
32598
  }
32312
32599
  return result;
32313
32600
  }
32314
- function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABEL_LINES2) {
32601
+ function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABEL_LINES3) {
32315
32602
  if (!text) return { lines: [""], fontSize: baseFontSize };
32316
32603
  const tokens = tokenize(text);
32317
32604
  for (let fs = baseFontSize; fs >= minFontSize; fs--) {
@@ -32326,7 +32613,7 @@ function wrapText(text, maxWidth, baseFontSize, minFontSize, maxLines = MAX_LABE
32326
32613
  if (allLines.length > maxLines) {
32327
32614
  const last = truncated[truncated.length - 1];
32328
32615
  if (!last.endsWith("\u2026")) {
32329
- const availWidth = maxWidth - H_PAD;
32616
+ const availWidth = maxWidth - H_PAD2;
32330
32617
  if (measureText(last + "\u2026", minFontSize) > availWidth) {
32331
32618
  truncated[truncated.length - 1] = truncateText(
32332
32619
  last,
@@ -32353,7 +32640,7 @@ function computeNodeText(label, description, depth, nodeWidth, hideDescriptions)
32353
32640
  nodeWidth,
32354
32641
  baseFontSize,
32355
32642
  MIN_FONT_SIZE,
32356
- MAX_LABEL_LINES2
32643
+ MAX_LABEL_LINES3
32357
32644
  );
32358
32645
  const descLines = [];
32359
32646
  let descFontSize = DESC_FONT_SIZE3;
@@ -32381,14 +32668,14 @@ function computeNodeText(label, description, depth, nodeWidth, hideDescriptions)
32381
32668
  descFontSize
32382
32669
  };
32383
32670
  }
32384
- var H_PAD, MAX_LABEL_LINES2, MAX_DESC_LINES3, ROOT_FONT_SIZE, MIN_FONT_SIZE, FONT_STEP, DESC_FONT_SIZE3;
32671
+ var H_PAD2, MAX_LABEL_LINES3, MAX_DESC_LINES3, ROOT_FONT_SIZE, MIN_FONT_SIZE, FONT_STEP, DESC_FONT_SIZE3;
32385
32672
  var init_text_wrap = __esm({
32386
32673
  "src/mindmap/text-wrap.ts"() {
32387
32674
  "use strict";
32388
32675
  init_description_helpers();
32389
32676
  init_text_measure();
32390
- H_PAD = 16;
32391
- MAX_LABEL_LINES2 = 3;
32677
+ H_PAD2 = 16;
32678
+ MAX_LABEL_LINES3 = 3;
32392
32679
  MAX_DESC_LINES3 = 2;
32393
32680
  ROOT_FONT_SIZE = 17;
32394
32681
  MIN_FONT_SIZE = 9;
@@ -32775,7 +33062,7 @@ function nodeHeightS(node, hideDescriptions, sc) {
32775
33062
  hideDescriptions
32776
33063
  );
32777
33064
  const labelLineCount = text.labelLines.length;
32778
- const labelH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT : LABEL_LINE_HEIGHT2 * labelLineCount;
33065
+ const labelH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT : LABEL_LINE_HEIGHT3 * labelLineCount;
32779
33066
  let h = labelH + NODE_V_PAD;
32780
33067
  if (text.descLines.length > 0) {
32781
33068
  h += DESC_LINE_HEIGHT3 * text.descLines.length + 4;
@@ -32795,7 +33082,7 @@ function populateDepthCache(roots) {
32795
33082
  };
32796
33083
  walk(roots, 0);
32797
33084
  }
32798
- var ROOT_WIDTH, DEPTH1_WIDTH, LEAF_WIDTH, SINGLE_LABEL_HEIGHT, LABEL_LINE_HEIGHT2, DESC_LINE_HEIGHT3, NODE_V_PAD, H_GAP2, V_GAP2, MARGIN6, MULTI_ROOT_GAP, nodeDepthCache;
33085
+ var ROOT_WIDTH, DEPTH1_WIDTH, LEAF_WIDTH, SINGLE_LABEL_HEIGHT, LABEL_LINE_HEIGHT3, DESC_LINE_HEIGHT3, NODE_V_PAD, H_GAP2, V_GAP2, MARGIN6, MULTI_ROOT_GAP, nodeDepthCache;
32799
33086
  var init_layout6 = __esm({
32800
33087
  "src/mindmap/layout.ts"() {
32801
33088
  "use strict";
@@ -32806,7 +33093,7 @@ var init_layout6 = __esm({
32806
33093
  DEPTH1_WIDTH = 150;
32807
33094
  LEAF_WIDTH = 120;
32808
33095
  SINGLE_LABEL_HEIGHT = 28;
32809
- LABEL_LINE_HEIGHT2 = 18;
33096
+ LABEL_LINE_HEIGHT3 = 18;
32810
33097
  DESC_LINE_HEIGHT3 = 14;
32811
33098
  NODE_V_PAD = 10;
32812
33099
  H_GAP2 = 40;
@@ -33097,18 +33384,18 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
33097
33384
  descFontSize
33098
33385
  } = textLayout;
33099
33386
  const labelLineCount = labelLines.length;
33100
- const labelZoneH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT2 : LABEL_LINE_HEIGHT3 * labelLineCount;
33387
+ const labelZoneH = labelLineCount <= 1 ? SINGLE_LABEL_HEIGHT2 : LABEL_LINE_HEIGHT4 * labelLineCount;
33101
33388
  const labelZoneHeight = showDesc ? labelZoneH : node.height;
33102
33389
  const centerX = node.x + node.width / 2;
33103
33390
  if (labelLineCount <= 1) {
33104
33391
  const labelY = node.y + labelZoneHeight / 2 + fontSize * 0.35;
33105
33392
  nodeG.append("text").attr("x", centerX).attr("y", labelY).attr("text-anchor", "middle").attr("font-size", fontSize).attr("font-weight", isRoot ? "bold" : "normal").attr("fill", onFillText).text(labelLines[0]);
33106
33393
  } else {
33107
- const blockH = LABEL_LINE_HEIGHT3 * (labelLineCount - 1);
33394
+ const blockH = LABEL_LINE_HEIGHT4 * (labelLineCount - 1);
33108
33395
  const firstBaselineY = node.y + labelZoneHeight / 2 - blockH / 2 + fontSize * 0.35;
33109
33396
  const textEl = nodeG.append("text").attr("x", centerX).attr("text-anchor", "middle").attr("font-size", fontSize).attr("font-weight", isRoot ? "bold" : "normal").attr("fill", onFillText);
33110
33397
  for (let i = 0; i < labelLines.length; i++) {
33111
- textEl.append("tspan").attr("x", centerX).attr("y", firstBaselineY + i * LABEL_LINE_HEIGHT3).text(labelLines[i]);
33398
+ textEl.append("tspan").attr("x", centerX).attr("y", firstBaselineY + i * LABEL_LINE_HEIGHT4).text(labelLines[i]);
33112
33399
  }
33113
33400
  }
33114
33401
  if (labelLines.length > 1 || labelLines[0] !== node.label) {
@@ -33183,7 +33470,7 @@ function renderMindmapForExport(content, theme, palette) {
33183
33470
  return extractExportSvg(container, theme);
33184
33471
  });
33185
33472
  }
33186
- var d3Selection12, DIAGRAM_PADDING7, TITLE_HEIGHT4, SINGLE_LABEL_HEIGHT2, LABEL_LINE_HEIGHT3, DESC_LINE_HEIGHT4, NODE_RADIUS, ROOT_STROKE_WIDTH, DEPTH_COLOR_KEYS;
33473
+ var d3Selection12, DIAGRAM_PADDING7, TITLE_HEIGHT4, SINGLE_LABEL_HEIGHT2, LABEL_LINE_HEIGHT4, DESC_LINE_HEIGHT4, NODE_RADIUS, ROOT_STROKE_WIDTH, DEPTH_COLOR_KEYS;
33187
33474
  var init_renderer7 = __esm({
33188
33475
  "src/mindmap/renderer.ts"() {
33189
33476
  "use strict";
@@ -33206,7 +33493,7 @@ var init_renderer7 = __esm({
33206
33493
  DIAGRAM_PADDING7 = 20;
33207
33494
  TITLE_HEIGHT4 = 30;
33208
33495
  SINGLE_LABEL_HEIGHT2 = 28;
33209
- LABEL_LINE_HEIGHT3 = 18;
33496
+ LABEL_LINE_HEIGHT4 = 18;
33210
33497
  DESC_LINE_HEIGHT4 = 14;
33211
33498
  NODE_RADIUS = 6;
33212
33499
  ROOT_STROKE_WIDTH = 2.5;
@@ -35873,7 +36160,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
35873
36160
  const labelText = edge.label ?? "";
35874
36161
  const techText = edge.technology ? `[${edge.technology}]` : "";
35875
36162
  const textW = Math.max(
35876
- measureText(labelText, EDGE_LABEL_FONT_SIZE6),
36163
+ measureText(labelText, EDGE_LABEL_FONT_SIZE7),
35877
36164
  measureText(techText, TECH_FONT_SIZE)
35878
36165
  );
35879
36166
  const bgW = textW + 12;
@@ -35884,7 +36171,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
35884
36171
  textY = midPt.y - 4;
35885
36172
  }
35886
36173
  if (labelText) {
35887
- edgeG.append("text").attr("x", midPt.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", EDGE_LABEL_FONT_SIZE6).attr("class", "c4-edge-label").text(labelText);
36174
+ edgeG.append("text").attr("x", midPt.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("class", "c4-edge-label").text(labelText);
35888
36175
  }
35889
36176
  if (techText) {
35890
36177
  edgeG.append("text").attr("x", midPt.x).attr("y", labelText ? textY + 18 : textY + 4).attr("text-anchor", "middle").attr("fill", edgeColor3).attr("font-size", TECH_FONT_SIZE).attr("font-style", "italic").attr("class", "c4-edge-tech").text(techText);
@@ -36068,7 +36355,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36068
36355
  const labelText = edge.label ?? "";
36069
36356
  const techText = edge.technology ? `[${edge.technology}]` : "";
36070
36357
  const textW = Math.max(
36071
- measureText(labelText, EDGE_LABEL_FONT_SIZE6),
36358
+ measureText(labelText, EDGE_LABEL_FONT_SIZE7),
36072
36359
  measureText(techText, TECH_FONT_SIZE)
36073
36360
  );
36074
36361
  const bgW = textW + 12;
@@ -36086,7 +36373,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36086
36373
  });
36087
36374
  }
36088
36375
  }
36089
- placeEdgeLabels(pendingLabels, edges, obstacleRects);
36376
+ placeEdgeLabels2(pendingLabels, edges, obstacleRects);
36090
36377
  for (const lbl of pendingLabels) {
36091
36378
  lbl.edgeG.append("rect").attr("x", lbl.x - lbl.bgW / 2).attr("y", lbl.y - lbl.bgH / 2).attr("width", lbl.bgW).attr("height", lbl.bgH).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.9).attr("class", "c4-edge-label-bg");
36092
36379
  let textY = lbl.y;
@@ -36094,7 +36381,7 @@ function renderEdges(contentG, edges, palette, onClickItem, obstacleRects) {
36094
36381
  textY = lbl.y - 4;
36095
36382
  }
36096
36383
  if (lbl.labelText) {
36097
- lbl.edgeG.append("text").attr("x", lbl.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", EDGE_LABEL_FONT_SIZE6).attr("class", "c4-edge-label").text(lbl.labelText);
36384
+ lbl.edgeG.append("text").attr("x", lbl.x).attr("y", textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", EDGE_LABEL_FONT_SIZE7).attr("class", "c4-edge-label").text(lbl.labelText);
36098
36385
  }
36099
36386
  if (lbl.techText) {
36100
36387
  lbl.edgeG.append("text").attr("x", lbl.x).attr("y", lbl.labelText ? textY + 18 : textY + 4).attr("text-anchor", "middle").attr("fill", lbl.edgeColor).attr("font-size", TECH_FONT_SIZE).attr("font-style", "italic").attr("class", "c4-edge-tech").text(lbl.techText);
@@ -36175,7 +36462,7 @@ function tangentAt(points, t) {
36175
36462
  }
36176
36463
  return { x: 0, y: 1 };
36177
36464
  }
36178
- function placeEdgeLabels(labels, edges, obstacleRects) {
36465
+ function placeEdgeLabels2(labels, edges, obstacleRects) {
36179
36466
  if (labels.length === 0) return;
36180
36467
  const allPaths = edges.map((e) => e.points);
36181
36468
  const placedRects = [];
@@ -36713,7 +37000,7 @@ function renderC4DeploymentForExport(content, theme, palette) {
36713
37000
  document.body.removeChild(el);
36714
37001
  }
36715
37002
  }
36716
- var d3Selection14, d3Shape6, DIAGRAM_PADDING8, MAX_SCALE5, TITLE_HEIGHT6, TYPE_FONT_SIZE, NAME_FONT_SIZE2, DESC_FONT_SIZE5, DESC_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE6, TECH_FONT_SIZE, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, BOUNDARY_LABEL_FONT_SIZE, BOUNDARY_STROKE_WIDTH, BOUNDARY_RADIUS, DRILL_BAR_HEIGHT, CYLINDER_RY, PERSON_HEAD_R, PERSON_ARM_SPAN, PERSON_LEG_SPAN, PERSON_ICON_W, PERSON_SW, lineGenerator4;
37003
+ var d3Selection14, d3Shape6, DIAGRAM_PADDING8, MAX_SCALE5, TITLE_HEIGHT6, TYPE_FONT_SIZE, NAME_FONT_SIZE2, DESC_FONT_SIZE5, DESC_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE7, TECH_FONT_SIZE, CARD_H_PAD4, CARD_V_PAD4, TYPE_LABEL_HEIGHT2, DIVIDER_GAP2, NAME_HEIGHT2, BOUNDARY_LABEL_FONT_SIZE, BOUNDARY_STROKE_WIDTH, BOUNDARY_RADIUS, DRILL_BAR_HEIGHT, CYLINDER_RY, PERSON_HEAD_R, PERSON_ARM_SPAN, PERSON_LEG_SPAN, PERSON_ICON_W, PERSON_SW, lineGenerator4;
36717
37004
  var init_renderer9 = __esm({
36718
37005
  "src/c4/renderer.ts"() {
36719
37006
  "use strict";
@@ -36738,7 +37025,7 @@ var init_renderer9 = __esm({
36738
37025
  NAME_FONT_SIZE2 = 14;
36739
37026
  DESC_FONT_SIZE5 = 11;
36740
37027
  DESC_LINE_HEIGHT6 = 16;
36741
- EDGE_LABEL_FONT_SIZE6 = 11;
37028
+ EDGE_LABEL_FONT_SIZE7 = 11;
36742
37029
  TECH_FONT_SIZE = 10;
36743
37030
  CARD_H_PAD4 = 20;
36744
37031
  CARD_V_PAD4 = 14;
@@ -37362,7 +37649,7 @@ function renderFlowchart(container, graph, layout, palette, isDark, onClickItem,
37362
37649
  const sTitleFontSize = ctx.text(TITLE_FONT_SIZE);
37363
37650
  const sTitleY = ctx.structural(TITLE_Y);
37364
37651
  const sNodeFontSize = ctx.text(NODE_FONT_SIZE2);
37365
- const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE7);
37652
+ const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE8);
37366
37653
  const sEdgeStrokeWidth = ctx.structural(EDGE_STROKE_WIDTH);
37367
37654
  const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
37368
37655
  const sArrowheadW = ctx.structural(ARROWHEAD_W3);
@@ -37588,7 +37875,7 @@ function renderFlowchartForExport(content, theme, palette) {
37588
37875
  document.body.removeChild(container);
37589
37876
  }
37590
37877
  }
37591
- var d3Selection15, DIAGRAM_PADDING9, MAX_SCALE6, NODE_FONT_SIZE2, EDGE_LABEL_FONT_SIZE7, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT;
37878
+ var d3Selection15, DIAGRAM_PADDING9, MAX_SCALE6, NODE_FONT_SIZE2, EDGE_LABEL_FONT_SIZE8, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT;
37592
37879
  var init_flowchart_renderer = __esm({
37593
37880
  "src/graph/flowchart-renderer.ts"() {
37594
37881
  "use strict";
@@ -37608,7 +37895,7 @@ var init_flowchart_renderer = __esm({
37608
37895
  DIAGRAM_PADDING9 = 20;
37609
37896
  MAX_SCALE6 = 3;
37610
37897
  NODE_FONT_SIZE2 = 13;
37611
- EDGE_LABEL_FONT_SIZE7 = 11;
37898
+ EDGE_LABEL_FONT_SIZE8 = 11;
37612
37899
  ARROWHEAD_W3 = 10;
37613
37900
  ARROWHEAD_H3 = 7;
37614
37901
  IO_SKEW = 15;
@@ -39042,7 +39329,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
39042
39329
  const midIdx = Math.floor(edge.points.length / 2);
39043
39330
  const midPt = edge.points[midIdx];
39044
39331
  if (midPt) {
39045
- const halfWidth = (measureText(edge.label, EDGE_LABEL_FONT_SIZE8) + 8) / 2;
39332
+ const halfWidth = (measureText(edge.label, EDGE_LABEL_FONT_SIZE9) + 8) / 2;
39046
39333
  if (midPt.x - halfWidth < minX) minX = midPt.x - halfWidth;
39047
39334
  if (midPt.x + halfWidth > maxX) maxX = midPt.x + halfWidth;
39048
39335
  }
@@ -39076,7 +39363,7 @@ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
39076
39363
  height: totalHeight
39077
39364
  };
39078
39365
  }
39079
- var import_dagre7, MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT5, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT3, NODE_FONT_SIZE3, META_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, PADDING_X3, GROUP_PADDING2, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES, GROUP_GAP2;
39366
+ var import_dagre7, MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT5, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT3, NODE_FONT_SIZE3, META_FONT_SIZE5, EDGE_LABEL_FONT_SIZE9, PADDING_X3, GROUP_PADDING2, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES, GROUP_GAP2;
39080
39367
  var init_layout10 = __esm({
39081
39368
  "src/infra/layout.ts"() {
39082
39369
  "use strict";
@@ -39091,7 +39378,7 @@ var init_layout10 = __esm({
39091
39378
  COLLAPSE_BAR_HEIGHT3 = 6;
39092
39379
  NODE_FONT_SIZE3 = 13;
39093
39380
  META_FONT_SIZE5 = 10;
39094
- EDGE_LABEL_FONT_SIZE8 = 11;
39381
+ EDGE_LABEL_FONT_SIZE9 = 11;
39095
39382
  PADDING_X3 = 24;
39096
39383
  GROUP_PADDING2 = 20;
39097
39384
  GROUP_HEADER_HEIGHT = 24;
@@ -39196,7 +39483,7 @@ function buildScaledConstants(ctx) {
39196
39483
  sNodeFontSize: ctx.text(NODE_FONT_SIZE4),
39197
39484
  sMetaFontSize: ctx.text(META_FONT_SIZE6),
39198
39485
  sMetaLineHeight: ctx.structural(META_LINE_HEIGHT6),
39199
- sEdgeLabelFontSize: ctx.text(EDGE_LABEL_FONT_SIZE9),
39486
+ sEdgeLabelFontSize: ctx.text(EDGE_LABEL_FONT_SIZE10),
39200
39487
  sGroupLabelFontSize: ctx.text(GROUP_LABEL_FONT_SIZE2),
39201
39488
  sNodeBorderRadius: ctx.structural(NODE_BORDER_RADIUS),
39202
39489
  sEdgeStrokeWidth: ctx.structural(EDGE_STROKE_WIDTH),
@@ -39925,7 +40212,7 @@ function renderEdgeLabels(svg, edges, nodes, groups, palette, _isDark, animate,
39925
40212
  if (!midPt) continue;
39926
40213
  const labelText = edge.label;
39927
40214
  const g = svg.append("g").attr("class", animate ? "infra-edge-label" : "");
39928
- const textWidth = measureText(labelText, EDGE_LABEL_FONT_SIZE9) + 8;
40215
+ const textWidth = measureText(labelText, EDGE_LABEL_FONT_SIZE10) + 8;
39929
40216
  g.append("rect").attr("x", midPt.x - textWidth / 2).attr("y", midPt.y - 8).attr("width", textWidth).attr("height", 16).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.9);
39930
40217
  g.append("text").attr("x", midPt.x).attr("y", midPt.y + 4).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", sc.sEdgeLabelFontSize).attr("fill", palette.textMuted).text(labelText);
39931
40218
  if (animate) {
@@ -40535,7 +40822,7 @@ function parseAndLayoutInfra(content) {
40535
40822
  const layout = layoutInfra(computed);
40536
40823
  return { parsed, computed, layout };
40537
40824
  }
40538
- var d3Selection16, d3Shape8, NODE_FONT_SIZE4, META_FONT_SIZE6, META_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE9, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, LEGEND_FIXED_GAP3, SPEED_BADGE_H_PAD, SPEED_BADGE_V_PAD, SPEED_BADGE_GAP, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, DESC_MAX_WIDTH, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
40825
+ var d3Selection16, d3Shape8, NODE_FONT_SIZE4, META_FONT_SIZE6, META_LINE_HEIGHT6, EDGE_LABEL_FONT_SIZE10, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, LEGEND_FIXED_GAP3, SPEED_BADGE_H_PAD, SPEED_BADGE_V_PAD, SPEED_BADGE_GAP, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, PROP_DISPLAY, DESC_MAX_CHARS, DESC_MAX_WIDTH, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
40539
40826
  var init_renderer10 = __esm({
40540
40827
  "src/infra/renderer.ts"() {
40541
40828
  "use strict";
@@ -40561,7 +40848,7 @@ var init_renderer10 = __esm({
40561
40848
  NODE_FONT_SIZE4 = 13;
40562
40849
  META_FONT_SIZE6 = 10;
40563
40850
  META_LINE_HEIGHT6 = 14;
40564
- EDGE_LABEL_FONT_SIZE9 = 11;
40851
+ EDGE_LABEL_FONT_SIZE10 = 11;
40565
40852
  GROUP_LABEL_FONT_SIZE2 = 14;
40566
40853
  NODE_BORDER_RADIUS = 8;
40567
40854
  OVERLOAD_STROKE_WIDTH = 3;
@@ -46949,7 +47236,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
46949
47236
  const sTitleFontSize = ctx.text(TITLE_FONT_SIZE);
46950
47237
  const sTitleY = ctx.structural(TITLE_Y);
46951
47238
  const sNodeFontSize = ctx.text(NODE_FONT_SIZE6);
46952
- const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE10);
47239
+ const sEdgeLabelFontSize = ctx.text(EDGE_LABEL_FONT_SIZE11);
46953
47240
  const sGroupLabelFontSize = ctx.text(GROUP_LABEL_FONT_SIZE3);
46954
47241
  const sEdgeStrokeWidth = ctx.structural(EDGE_STROKE_WIDTH);
46955
47242
  const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
@@ -47229,7 +47516,7 @@ function renderStateForExport(content, theme, palette) {
47229
47516
  document.body.removeChild(container);
47230
47517
  }
47231
47518
  }
47232
- var d3Selection19, DIAGRAM_PADDING12, MAX_SCALE7, NODE_FONT_SIZE6, EDGE_LABEL_FONT_SIZE10, GROUP_LABEL_FONT_SIZE3, ARROWHEAD_W5, ARROWHEAD_H5, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING;
47519
+ var d3Selection19, DIAGRAM_PADDING12, MAX_SCALE7, NODE_FONT_SIZE6, EDGE_LABEL_FONT_SIZE11, GROUP_LABEL_FONT_SIZE3, ARROWHEAD_W5, ARROWHEAD_H5, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING;
47233
47520
  var init_state_renderer = __esm({
47234
47521
  "src/graph/state-renderer.ts"() {
47235
47522
  "use strict";
@@ -47249,7 +47536,7 @@ var init_state_renderer = __esm({
47249
47536
  DIAGRAM_PADDING12 = 20;
47250
47537
  MAX_SCALE7 = 3;
47251
47538
  NODE_FONT_SIZE6 = 13;
47252
- EDGE_LABEL_FONT_SIZE10 = 11;
47539
+ EDGE_LABEL_FONT_SIZE11 = 11;
47253
47540
  GROUP_LABEL_FONT_SIZE3 = 11;
47254
47541
  ARROWHEAD_W5 = 10;
47255
47542
  ARROWHEAD_H5 = 7;
@@ -50071,10 +50358,10 @@ function renderedDescNodeHeight(numLines, scale) {
50071
50358
  return headerH + textBlockH + bottomPad;
50072
50359
  }
50073
50360
  function wrapEdgeLabelText(label, description, maxWidth = EDGE_LABEL_MAX_WIDTH) {
50074
- const labelLines = label ? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE11, maxWidth) : [];
50361
+ const labelLines = label ? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE12, maxWidth) : [];
50075
50362
  const descLines = [];
50076
50363
  for (const d of description) {
50077
- descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE11, maxWidth));
50364
+ descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE12, maxWidth));
50078
50365
  }
50079
50366
  return { labelLines, descLines };
50080
50367
  }
@@ -50212,9 +50499,9 @@ function computeEdgePaths(layoutNodes, parsed, cx, cy, radius, isClockwise) {
50212
50499
  const lineCount = labelLines.length + descLines.length;
50213
50500
  let labelPxW = 0;
50214
50501
  for (const l of labelLines)
50215
- labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE11));
50502
+ labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE12));
50216
50503
  for (const l of descLines)
50217
- labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE11));
50504
+ labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE12));
50218
50505
  const { labelX, labelY, labelAngle } = computeEdgeLabelPosition(
50219
50506
  midAngle,
50220
50507
  radius,
@@ -50316,9 +50603,9 @@ function fitToCanvas(nodes, edges, parsed, _cx, _cy, radius, width, height, _isC
50316
50603
  );
50317
50604
  let textWidth = 0;
50318
50605
  for (const l of labelLines)
50319
- textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE11));
50606
+ textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE12));
50320
50607
  for (const l of descLines)
50321
- textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE11));
50608
+ textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE12));
50322
50609
  if (textWidth === 0) continue;
50323
50610
  const normAngle = (le.labelAngle % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
50324
50611
  const isRight = normAngle < Math.PI * 0.4 || normAngle > Math.PI * 1.6;
@@ -50392,7 +50679,7 @@ function buildEdgeArc(src, tgt, cx, cy, radius, isClockwise, arrowLength = 0) {
50392
50679
  const midAngle = srcExitAngle + dir * visibleSweep / 2;
50393
50680
  return { path, midAngle };
50394
50681
  }
50395
- var MIN_ARC_ANGLE, LABEL_FONT_SIZE4, CIRCLE_LABEL_FONT_SIZE, DESC_FONT_SIZE6, EDGE_LABEL_FONT_SIZE11, MIN_NODE_WIDTH4, MAX_NODE_WIDTH3, DESC_MIN_WIDTH, DESC_WIDTH_STEP, DESC_TARGET_RATIO, PLAIN_NODE_HEIGHT, HEADER_HEIGHT4, DESC_LINE_HEIGHT7, DESC_PAD_Y, NODE_PAD_X, MIN_CIRCLE_RADIUS, CIRCLE_PAD, RENDERER_DESC_FONT, RENDERER_DESC_FONT_MIN, RENDERER_DESC_LINE_H, RENDERER_DESC_LINE_H_MIN, EDGE_LABEL_MAX_WIDTH;
50682
+ var MIN_ARC_ANGLE, LABEL_FONT_SIZE4, CIRCLE_LABEL_FONT_SIZE, DESC_FONT_SIZE6, EDGE_LABEL_FONT_SIZE12, MIN_NODE_WIDTH4, MAX_NODE_WIDTH3, DESC_MIN_WIDTH, DESC_WIDTH_STEP, DESC_TARGET_RATIO, PLAIN_NODE_HEIGHT, HEADER_HEIGHT4, DESC_LINE_HEIGHT7, DESC_PAD_Y, NODE_PAD_X, MIN_CIRCLE_RADIUS, CIRCLE_PAD, RENDERER_DESC_FONT, RENDERER_DESC_FONT_MIN, RENDERER_DESC_LINE_H, RENDERER_DESC_LINE_H_MIN, EDGE_LABEL_MAX_WIDTH;
50396
50683
  var init_layout14 = __esm({
50397
50684
  "src/cycle/layout.ts"() {
50398
50685
  "use strict";
@@ -50403,7 +50690,7 @@ var init_layout14 = __esm({
50403
50690
  LABEL_FONT_SIZE4 = 13;
50404
50691
  CIRCLE_LABEL_FONT_SIZE = 16;
50405
50692
  DESC_FONT_SIZE6 = 11;
50406
- EDGE_LABEL_FONT_SIZE11 = 11;
50693
+ EDGE_LABEL_FONT_SIZE12 = 11;
50407
50694
  MIN_NODE_WIDTH4 = 70;
50408
50695
  MAX_NODE_WIDTH3 = 260;
50409
50696
  DESC_MIN_WIDTH = 140;
@@ -50548,7 +50835,7 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
50548
50835
  const scaledNodeFont = Math.max(6, Math.round(NODE_FONT_SIZE7 * layout.scale));
50549
50836
  const scaledEdgeLabelFont = Math.max(
50550
50837
  6,
50551
- Math.round(EDGE_LABEL_FONT_SIZE12 * layout.scale)
50838
+ Math.round(EDGE_LABEL_FONT_SIZE13 * layout.scale)
50552
50839
  );
50553
50840
  const CIRCLE_LABEL_FONT_SIZE2 = 16;
50554
50841
  const scaledCircleLabelFont = Math.max(
@@ -50711,7 +50998,7 @@ function ensureArrowMarker(defs, color, strokeWidth) {
50711
50998
  const mh = Math.max(1.5, mw * 0.5);
50712
50999
  defs.append("marker").attr("id", id).attr("viewBox", `0 0 ${mw} ${mh}`).attr("refX", mw * 0.1).attr("refY", mh / 2).attr("markerWidth", mw).attr("markerHeight", mh).attr("orient", "auto").append("polygon").attr("points", `0,0 ${mw},${mh / 2} 0,${mh}`).attr("fill", color);
50713
51000
  }
50714
- var d3Selection22, NODE_FONT_SIZE7, DESC_FONT_SIZE7, EDGE_LABEL_FONT_SIZE12, DESC_LINE_HEIGHT8, TITLE_AREA_HEIGHT;
51001
+ var d3Selection22, NODE_FONT_SIZE7, DESC_FONT_SIZE7, EDGE_LABEL_FONT_SIZE13, DESC_LINE_HEIGHT8, TITLE_AREA_HEIGHT;
50715
51002
  var init_renderer15 = __esm({
50716
51003
  "src/cycle/renderer.ts"() {
50717
51004
  "use strict";
@@ -50729,7 +51016,7 @@ var init_renderer15 = __esm({
50729
51016
  init_text_measure();
50730
51017
  NODE_FONT_SIZE7 = 13;
50731
51018
  DESC_FONT_SIZE7 = 11;
50732
- EDGE_LABEL_FONT_SIZE12 = 11;
51019
+ EDGE_LABEL_FONT_SIZE13 = 11;
50733
51020
  DESC_LINE_HEIGHT8 = 15;
50734
51021
  TITLE_AREA_HEIGHT = 50;
50735
51022
  }
@@ -51127,7 +51414,7 @@ function resolveMap(parsed, data) {
51127
51414
  const hint = suggest(r.name, allNames);
51128
51415
  err(
51129
51416
  r.lineNumber,
51130
- `Unknown subdivision "${r.name}".${hint ? " " + hint : ""}`,
51417
+ `Unknown region "${r.name}" \u2014 not a known country or US state.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${r.name}"\` (or the lookup_map_location tool), use an ISO code (e.g. FR, US-CA), or coordinates.`,
51131
51418
  "E_MAP_UNKNOWN_SUBDIVISION"
51132
51419
  );
51133
51420
  continue;
@@ -51207,7 +51494,7 @@ function resolveMap(parsed, data) {
51207
51494
  const hint = suggest(name, cityNames);
51208
51495
  err(
51209
51496
  line11,
51210
- `Unknown place "${name}" (not in the gazetteer; use coordinates).${hint ? " " + hint : ""}`,
51497
+ `Unknown place "${name}" \u2014 not in the gazetteer.${hint ? " " + hint : ""} Search the exact token with \`dgmo map-search "${name}"\` (or the lookup_map_location tool), or use coordinates \`poi <lat> <lon>\`.`,
51211
51498
  "E_MAP_UNKNOWN_PLACE"
51212
51499
  );
51213
51500
  return { kind: "miss" };
@@ -51749,6 +52036,21 @@ function mapLegendBand(legend, opts) {
51749
52036
  if (height <= 0) return 0;
51750
52037
  return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
51751
52038
  }
52039
+ function mapLegendBox(legend, opts) {
52040
+ if (!legend) return null;
52041
+ const groups = mapLegendGroups(legend);
52042
+ if (groups.length === 0) return null;
52043
+ const config = mapLegendConfig(groups, opts.mode);
52044
+ const state = { activeGroup: legend.activeGroup };
52045
+ const { width, height } = computeLegendLayout(config, state, opts.width);
52046
+ if (height <= 0 || width <= 0) return null;
52047
+ return {
52048
+ x: (opts.width - width) / 2,
52049
+ y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
52050
+ width,
52051
+ height
52052
+ };
52053
+ }
51752
52054
  var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
51753
52055
  var init_legend_band = __esm({
51754
52056
  "src/map/legend-band.ts"() {
@@ -51873,6 +52175,8 @@ function placeContextLabels(args) {
51873
52175
  waterBodies,
51874
52176
  countries,
51875
52177
  palette,
52178
+ countryTone,
52179
+ waterTone,
51876
52180
  project,
51877
52181
  collides,
51878
52182
  contentPoints,
@@ -51882,8 +52186,8 @@ function placeContextLabels(args) {
51882
52186
  const band = tierBand(Math.max(dLonSpan, dLatSpan));
51883
52187
  const budget = labelBudget(width, height, band);
51884
52188
  if (budget <= 0) return [];
51885
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
51886
- const countryColor = palette.textMuted;
52189
+ const waterColor = waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
52190
+ const countryColor = countryTone ?? palette.textMuted;
51887
52191
  const haloColor = palette.bg;
51888
52192
  const candidates = [];
51889
52193
  const center = [width / 2, height / 2];
@@ -51972,8 +52276,7 @@ function placeContextLabels(args) {
51972
52276
  )
51973
52277
  );
51974
52278
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
51975
- const fade = Math.round(t * COUNTRY_FADE_MAX);
51976
- const color = fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
52279
+ const color = countryColor;
51977
52280
  const text = c.name;
51978
52281
  const tw = labelWidth(text, 0, fontSize);
51979
52282
  if (tw > w || fontSize + 2 * PADY > h) continue;
@@ -52067,7 +52370,7 @@ function placeContextLabels(args) {
52067
52370
  }
52068
52371
  return placed;
52069
52372
  }
52070
- var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, COUNTRY_FADE_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52373
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, COUNTRY_FONT_MAX, COUNTRY_SIZE_FRAC_MIN, COUNTRY_SIZE_FRAC_MAX, MAX_COUNTRY_POSITIONS, COUNTRY_POS_GRID, COUNTRY_POS_TOPN_MARGIN, KIND_ORDER;
52071
52374
  var init_context_labels = __esm({
52072
52375
  "src/map/context-labels.ts"() {
52073
52376
  "use strict";
@@ -52084,7 +52387,6 @@ var init_context_labels = __esm({
52084
52387
  COUNTRY_FONT_MAX = 22;
52085
52388
  COUNTRY_SIZE_FRAC_MIN = 0.06;
52086
52389
  COUNTRY_SIZE_FRAC_MAX = 0.32;
52087
- COUNTRY_FADE_MAX = 45;
52088
52390
  MAX_COUNTRY_POSITIONS = 4;
52089
52391
  COUNTRY_POS_GRID = 5;
52090
52392
  COUNTRY_POS_TOPN_MARGIN = 3;
@@ -52211,36 +52513,23 @@ function countryLabelPositions(args) {
52211
52513
  return n;
52212
52514
  };
52213
52515
  const dist2ToCentre = (c) => (c.sx - cx) ** 2 + (c.sy - cy) ** 2;
52214
- const pool = [...kept];
52215
- pool.sort((a, b) => {
52216
- const d = interiorness(b) - interiorness(a);
52217
- return d !== 0 ? d : dist2ToCentre(a) - dist2ToCentre(b);
52218
- });
52219
- const ordered = [pool.shift()];
52220
- while (ordered.length < MAX_COUNTRY_POSITIONS && pool.length) {
52221
- let bestIdx = 0;
52222
- let bestMin = -1;
52223
- for (let k = 0; k < pool.length; k++) {
52224
- const c = pool[k];
52225
- let minD = Infinity;
52226
- for (const o of ordered) {
52227
- const d = (c.sx - o.sx) ** 2 + (c.sy - o.sy) ** 2;
52228
- if (d < minD) minD = d;
52229
- }
52230
- if (minD > bestMin) {
52231
- bestMin = minD;
52232
- bestIdx = k;
52233
- }
52516
+ const centreLon = kept.reduce((s, c) => s + c.lon, 0) / kept.length;
52517
+ const centreLat = kept.reduce((s, c) => s + c.lat, 0) / kept.length;
52518
+ const centreScreen = project(centreLon, centreLat);
52519
+ const lead = pointInGeometry(geometry, centreLon, centreLat) && centreScreen && Number.isFinite(centreScreen[0]) && Number.isFinite(centreScreen[1]) && centreScreen[0] >= 0 && centreScreen[0] <= width && centreScreen[1] >= 0 && centreScreen[1] <= height ? [
52520
+ {
52521
+ lonLat: [centreLon, centreLat],
52522
+ screen: [centreScreen[0], centreScreen[1]]
52234
52523
  }
52235
- ordered.push(pool.splice(bestIdx, 1)[0]);
52236
- }
52237
- const grid = ordered.map((c) => ({
52524
+ ] : [];
52525
+ const grid = [...kept].sort(
52526
+ (a, b) => interiorness(b) - interiorness(a) || dist2ToCentre(a) - dist2ToCentre(b)
52527
+ ).map((c) => ({
52238
52528
  lonLat: [c.lon, c.lat],
52239
52529
  screen: [c.sx, c.sy]
52240
52530
  }));
52241
52531
  const curatedPos = curated ? mkCurated(curated, project) : [];
52242
- const out = [...curatedPos, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52243
- return out;
52532
+ return [...curatedPos, ...lead, ...grid].slice(0, MAX_COUNTRY_POSITIONS);
52244
52533
  }
52245
52534
  function mkCurated(curated, project) {
52246
52535
  if (!curated) return [];
@@ -52506,6 +52795,25 @@ function layoutMap(resolvedIn, data, size, opts) {
52506
52795
  palette.bg,
52507
52796
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
52508
52797
  );
52798
+ const contrastFloorTone = (tone, ...substrates) => {
52799
+ const clears = (t) => substrates.every((s) => contrastRatio(t, s) >= REGION_LABEL_HALO_RATIO);
52800
+ if (clears(tone)) return tone;
52801
+ for (let s = 10; s <= 100; s += 10) {
52802
+ const t = mix(palette.text, tone, s);
52803
+ if (clears(t)) return t;
52804
+ }
52805
+ return palette.text;
52806
+ };
52807
+ const backdropLandTone = contrastFloorTone(
52808
+ palette.textMuted,
52809
+ neutralFill,
52810
+ water
52811
+ );
52812
+ const backdropWaterTone = contrastFloorTone(
52813
+ mix(palette.colors.blue, palette.textMuted, 50),
52814
+ water,
52815
+ neutralFill
52816
+ );
52509
52817
  const hasDirectColor = resolved.regions.some((r) => r.color !== void 0);
52510
52818
  const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && !hasDirectColor && fillGroupNames.size === 0;
52511
52819
  const colorByIso = /* @__PURE__ */ new Map();
@@ -52620,6 +52928,33 @@ function layoutMap(resolvedIn, data, size, opts) {
52620
52928
  hasSubtitle: Boolean(resolved.subtitle)
52621
52929
  });
52622
52930
  if (legendBand > topPad) topPad = legendBand;
52931
+ const topReserved = [];
52932
+ if (shownTitle) {
52933
+ const lines = resolved.subtitle ? 2 : 1;
52934
+ const tw = Math.max(
52935
+ measureLegendText(shownTitle, TITLE_FONT_SIZE),
52936
+ resolved.subtitle ? measureLegendText(resolved.subtitle, TITLE_FONT_SIZE) : 0
52937
+ ) + 2 * TITLE_FONT_SIZE;
52938
+ topReserved.push({
52939
+ x: width / 2 - tw / 2,
52940
+ y: TITLE_Y - TITLE_FONT_SIZE,
52941
+ w: tw,
52942
+ h: TITLE_FONT_SIZE * (lines + 0.5)
52943
+ });
52944
+ }
52945
+ const legendBox = mapLegendBox(legend, {
52946
+ width,
52947
+ mode: opts.legendMode ?? "preview",
52948
+ hasTitle: Boolean(shownTitle),
52949
+ hasSubtitle: Boolean(resolved.subtitle)
52950
+ });
52951
+ if (legendBox)
52952
+ topReserved.push({
52953
+ x: legendBox.x,
52954
+ y: legendBox.y,
52955
+ w: legendBox.width,
52956
+ h: legendBox.height
52957
+ });
52623
52958
  const reserve = opts._calloutReserve;
52624
52959
  const fitLeft = FIT_PAD + (reserve?.left ?? 0);
52625
52960
  const fitRight = width - FIT_PAD - (reserve?.right ?? 0);
@@ -53349,6 +53684,19 @@ function layoutMap(resolvedIn, data, size, opts) {
53349
53684
  const by = b.cy - (b.cy - py) / tb * trimB;
53350
53685
  return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
53351
53686
  };
53687
+ const legLabelPoint = (a, b, curved, offset) => {
53688
+ const mx = (a.cx + b.cx) / 2;
53689
+ const my = (a.cy + b.cy) / 2;
53690
+ if (!curved && offset === 0) return { x: mx, y: my - 4 };
53691
+ const dx = b.cx - a.cx;
53692
+ const dy = b.cy - a.cy;
53693
+ const len = Math.hypot(dx, dy) || 1;
53694
+ const nx = -dy / len;
53695
+ const ny = dx / len;
53696
+ const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
53697
+ const off = bow * 0.5 + Math.sign(bow || 1) * 8;
53698
+ return { x: mx + nx * off, y: my + ny * off };
53699
+ };
53352
53700
  const routeLegVals = resolved.routes.flatMap((rt) => rt.legs).map((l) => Number(l.value)).filter((n) => Number.isFinite(n) && n > 0);
53353
53701
  const rlMin = routeLegVals.length ? Math.min(...routeLegVals) : 0;
53354
53702
  const rlMax = routeLegVals.length ? Math.max(...routeLegVals) : 0;
@@ -53362,13 +53710,12 @@ function layoutMap(resolvedIn, data, size, opts) {
53362
53710
  const a = poiScreen.get(leg.fromId);
53363
53711
  const b = poiScreen.get(leg.toId);
53364
53712
  if (!a || !b) continue;
53365
- const mx = (a.cx + b.cx) / 2;
53366
- const my = (a.cy + b.cy) / 2;
53713
+ const lp = legLabelPoint(a, b, leg.style === "arc", 0);
53367
53714
  const bow = {
53368
53715
  curved: leg.style === "arc",
53369
53716
  offset: 0,
53370
- labelX: mx,
53371
- labelY: my - 4
53717
+ labelX: lp.x,
53718
+ labelY: lp.y
53372
53719
  };
53373
53720
  const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53374
53721
  legs.push({
@@ -53415,13 +53762,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53415
53762
  const b = poiScreen.get(e.toId);
53416
53763
  if (!a || !b) return;
53417
53764
  const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
53418
- const mx = (a.cx + b.cx) / 2;
53419
- const my = (a.cy + b.cy) / 2;
53765
+ const curved = e.style === "arc" || n > 1;
53766
+ const lp = legLabelPoint(a, b, curved, fanOffset);
53420
53767
  const bow = {
53421
- curved: e.style === "arc" || n > 1,
53768
+ curved,
53422
53769
  offset: fanOffset,
53423
- labelX: mx,
53424
- labelY: my - 4
53770
+ labelX: lp.x,
53771
+ labelY: lp.y
53425
53772
  };
53426
53773
  const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
53427
53774
  legs.push({
@@ -53445,7 +53792,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53445
53792
  });
53446
53793
  }
53447
53794
  const labels = [];
53448
- const obstacles = [];
53795
+ const obstacles = [...topReserved];
53449
53796
  const regionLabelGuards = [];
53450
53797
  const markers = pois.map((p) => ({
53451
53798
  cx: p.cx,
@@ -53483,6 +53830,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53483
53830
  }
53484
53831
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
53485
53832
  const showRegionLabels = resolved.directives.noRegionLabels !== true;
53833
+ const labeledRegionIds = /* @__PURE__ */ new Set();
53486
53834
  const showRegionValues = resolved.directives.noRegionValue !== true && activeIsScore;
53487
53835
  const regionValueStr = (value) => showRegionValues && value !== void 0 ? compactNumber(value) : void 0;
53488
53836
  const isCompact = width < COMPACT_WIDTH_PX;
@@ -53503,7 +53851,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53503
53851
  const REGION_FONT_MAX_ORIENT = 22;
53504
53852
  const REGION_SIZE_FRAC_MIN = 0.06;
53505
53853
  const REGION_SIZE_FRAC_MAX = 0.32;
53506
- const REGION_FADE_ORIENT = 45;
53854
+ const SUBJECT_FONT_MAX = 18;
53855
+ const SUBJECT_MIN_PROMINENCE = 13;
53856
+ const SUBJECT_LEADER_FONT = 15;
53507
53857
  const canvasLinear = Math.sqrt(Math.max(1, width * height));
53508
53858
  const sizeT = (boxW, boxH) => {
53509
53859
  const frac = Math.sqrt(Math.max(0, boxW * boxH)) / canvasLinear;
@@ -53515,9 +53865,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53515
53865
  )
53516
53866
  );
53517
53867
  };
53518
- const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, fade = 0) => {
53868
+ const pushRegionLabel = (x, y, text, fill2, lineNumber, valueLine, fontSize = FONT2, colorOverride) => {
53519
53869
  const { color: baseColor, haloColor } = labelOnFill(fill2);
53520
- const color = fade > 0 ? mix(baseColor, palette.bg, fade) : baseColor;
53870
+ const color = colorOverride ?? baseColor;
53521
53871
  const vf = Math.round(fontSize * 0.82);
53522
53872
  const halfW = Math.max(
53523
53873
  measureLegendText(text, fontSize),
@@ -53532,7 +53882,11 @@ function layoutMap(resolvedIn, data, size, opts) {
53532
53882
  text,
53533
53883
  anchor: "middle",
53534
53884
  color,
53535
- halo: overflows,
53885
+ // A backdrop label (colorOverride) NEVER haloes: the placement gate above
53886
+ // guarantees it sits on a floored-readable substrate (own fill, neutral
53887
+ // land, foreign land, or water), so a halo is pure noise. Data labels keep
53888
+ // the containment-gated halo until the role-tier pass (Phase 3).
53889
+ halo: colorOverride ? false : overflows,
53536
53890
  haloColor,
53537
53891
  ...fontSize !== FONT2 && { fontSize },
53538
53892
  ...valueLine !== void 0 && { valueLine },
@@ -53568,7 +53922,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53568
53922
  if (!c || !Number.isFinite(c[0])) return null;
53569
53923
  return { r, c, boxW, boxH, area: boxW * boxH, candidates };
53570
53924
  }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
53571
- const placedRegionRects = [];
53925
+ const placedRegionRects = [...topReserved];
53572
53926
  const placedLeaders = [];
53573
53927
  const isEmptyFill = (f) => f === water || f === neutralFill || f === foreignFill;
53574
53928
  const POI_LABEL_PAD = 14;
@@ -53581,9 +53935,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53581
53935
  const SHORT_HOP_MAX = Math.max(46, Math.min(width, height) * 0.11);
53582
53936
  const STEP = 4;
53583
53937
  const HOP_GAP = 9;
53584
- const tryShortHopCallout = (cx, cy, fill2, name, value) => {
53585
- const chipW = stackW(name, value);
53586
- const chipH = stackH(value !== void 0);
53938
+ const tryShortHopCallout = (cx, cy, fill2, name, value, font = FONT2, gap = HOP_GAP, maxLen = SHORT_HOP_MAX) => {
53939
+ const chipW = stackW(name, value, font);
53940
+ const chipH = stackH(value !== void 0, font);
53587
53941
  const dirs = [
53588
53942
  [0, -1],
53589
53943
  // north (over Canada)
@@ -53599,7 +53953,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53599
53953
  let ex = cx;
53600
53954
  let ey = cy;
53601
53955
  let steps = 0;
53602
- const maxSteps = Math.ceil(SHORT_HOP_MAX / STEP) + 1;
53956
+ const maxSteps = Math.ceil(maxLen / STEP) + 1;
53603
53957
  while (steps < maxSteps && fillAt(ex, ey) === fill2) {
53604
53958
  ex += dx * STEP;
53605
53959
  ey += dy * STEP;
@@ -53607,8 +53961,8 @@ function layoutMap(resolvedIn, data, size, opts) {
53607
53961
  }
53608
53962
  if (fillAt(ex, ey) === fill2) continue;
53609
53963
  const halfAlong = (Math.abs(dx) * chipW + Math.abs(dy) * chipH) / 2;
53610
- const ccx = ex + dx * (HOP_GAP + halfAlong);
53611
- const ccy = ey + dy * (HOP_GAP + halfAlong);
53964
+ const ccx = ex + dx * (gap + halfAlong);
53965
+ const ccy = ey + dy * (gap + halfAlong);
53612
53966
  const rect = {
53613
53967
  x: ccx - chipW / 2,
53614
53968
  y: ccy - chipH / 2,
@@ -53633,7 +53987,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53633
53987
  const innerX = ccx - dx * (chipW / 2);
53634
53988
  const innerY = ccy - dy * (chipH / 2);
53635
53989
  const len = Math.hypot(innerX - cx, innerY - cy);
53636
- if (len > SHORT_HOP_MAX) continue;
53990
+ if (len > maxLen) continue;
53637
53991
  if (placedLeaders.some(
53638
53992
  (l) => segmentsCross(cx, cy, innerX, innerY, l[0], l[1], l[2], l[3])
53639
53993
  ))
@@ -53657,6 +54011,78 @@ function layoutMap(resolvedIn, data, size, opts) {
53657
54011
  const valStr = regionValueStr(r.value);
53658
54012
  const fitsRegions = (rect) => !placedRegionRects.some((p) => rectsOverlap(rect, p));
53659
54013
  const fitsPois = (rect) => !poiObstacles.some((o) => rectsOverlap(rect, o));
54014
+ const isSubject = regionById.has(r.id);
54015
+ const extremesOnFill = (ax, ay, text) => {
54016
+ const halfW = measureLegendText(text, FONT2) / 2;
54017
+ return fillAt(ax - halfW, ay) === r.fill && fillAt(ax + halfW, ay) === r.fill;
54018
+ };
54019
+ if (isSubject && valStr === void 0 && r.label !== void 0) {
54020
+ const name = candidates[0];
54021
+ let best = -1;
54022
+ for (let f = SUBJECT_FONT_MAX; f >= FONT2; f--) {
54023
+ if (measureLegendText(name, f) > boxW || f + 2 * LABEL_PADY > boxH)
54024
+ continue;
54025
+ const rect = regionLabelRect(c[0], c[1], name, void 0, f);
54026
+ if (!fitsRegions(rect) || !fitsPois(rect) || collides(rect)) continue;
54027
+ const halfW = measureLegendText(name, f) / 2;
54028
+ if (fillAt(c[0] - halfW, c[1]) !== r.fill || fillAt(c[0] + halfW, c[1]) !== r.fill)
54029
+ continue;
54030
+ best = f;
54031
+ break;
54032
+ }
54033
+ if (best >= SUBJECT_MIN_PROMINENCE) {
54034
+ const rect = regionLabelRect(c[0], c[1], name, void 0, best);
54035
+ placedRegionRects.push(rect);
54036
+ pushRegionLabel(
54037
+ c[0],
54038
+ c[1],
54039
+ name,
54040
+ r.fill,
54041
+ r.lineNumber,
54042
+ void 0,
54043
+ best
54044
+ );
54045
+ labeledRegionIds.add(r.id);
54046
+ regionLabelGuards.push({ label: labels[labels.length - 1], rect });
54047
+ } else {
54048
+ const hop = tryShortHopCallout(
54049
+ c[0],
54050
+ c[1],
54051
+ r.fill,
54052
+ r.label,
54053
+ void 0,
54054
+ SUBJECT_LEADER_FONT,
54055
+ HOP_GAP + SUBJECT_LEADER_FONT,
54056
+ SHORT_HOP_MAX * 2.2
54057
+ );
54058
+ if (hop) {
54059
+ placedRegionRects.push(hop.rect);
54060
+ placedLeaders.push(hop.leader);
54061
+ const dark = mix(r.fill, palette.text, 60);
54062
+ labels.push({
54063
+ x: hop.x,
54064
+ y: hop.y,
54065
+ text: r.label,
54066
+ anchor: "middle",
54067
+ color: palette.text,
54068
+ halo: false,
54069
+ haloColor: palette.bg,
54070
+ fontSize: SUBJECT_LEADER_FONT,
54071
+ leader: {
54072
+ x1: hop.leader[0],
54073
+ y1: hop.leader[1],
54074
+ x2: hop.leader[2],
54075
+ y2: hop.leader[3]
54076
+ },
54077
+ leaderColor: dark,
54078
+ calloutDot: { x: c[0], y: c[1], color: dark },
54079
+ lineNumber: r.lineNumber
54080
+ });
54081
+ labeledRegionIds.add(r.id);
54082
+ }
54083
+ }
54084
+ continue;
54085
+ }
53660
54086
  const seekAnchors = [
53661
54087
  { x: c[0], y: c[1], guard: false }
53662
54088
  ];
@@ -53679,13 +54105,13 @@ function layoutMap(resolvedIn, data, size, opts) {
53679
54105
  const nameRect = regionLabelRect(a.x, a.y, t);
53680
54106
  if (valStr && stackW(t, valStr) <= boxW && stackH(true) <= boxH) {
53681
54107
  const stackRect = regionLabelRect(a.x, a.y, t, valStr);
53682
- if (fitsRegions(stackRect) && fitsPois(nameRect)) {
54108
+ if (fitsRegions(stackRect) && fitsPois(nameRect) && extremesOnFill(a.x, a.y, t)) {
53683
54109
  chosen = { text: t, valueLine: valStr, ax: a.x, ay: a.y };
53684
54110
  break;
53685
54111
  }
53686
54112
  }
53687
54113
  if (labelW(t) <= boxW && labelH <= boxH) {
53688
- if (fitsRegions(nameRect) && fitsPois(nameRect)) {
54114
+ if (fitsRegions(nameRect) && fitsPois(nameRect) && (valStr === void 0 || extremesOnFill(a.x, a.y, t))) {
53689
54115
  chosen = { text: t, ax: a.x, ay: a.y };
53690
54116
  break;
53691
54117
  }
@@ -53693,7 +54119,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53693
54119
  }
53694
54120
  if (chosen) break;
53695
54121
  }
53696
- if (chosen === void 0 && valStr && r.label !== void 0) {
54122
+ if (chosen === void 0 && r.label !== void 0 && isSubject) {
53697
54123
  const hop = tryShortHopCallout(c[0], c[1], r.fill, r.label, valStr);
53698
54124
  if (hop) {
53699
54125
  placedRegionRects.push(hop.rect);
@@ -53705,9 +54131,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53705
54131
  text: r.label,
53706
54132
  anchor: "middle",
53707
54133
  color: palette.text,
53708
- halo: true,
54134
+ halo: false,
53709
54135
  haloColor: palette.bg,
53710
- valueLine: valStr,
54136
+ ...valStr !== void 0 && { valueLine: valStr },
53711
54137
  leader: {
53712
54138
  x1: hop.leader[0],
53713
54139
  y1: hop.leader[1],
@@ -53718,20 +54144,28 @@ function layoutMap(resolvedIn, data, size, opts) {
53718
54144
  calloutDot: { x: c[0], y: c[1], color: dark },
53719
54145
  lineNumber: r.lineNumber
53720
54146
  });
54147
+ labeledRegionIds.add(r.id);
53721
54148
  }
53722
54149
  continue;
53723
54150
  }
53724
54151
  if (chosen === void 0) continue;
53725
54152
  const isOrient = r.value === void 0 && r.layer === "base";
53726
54153
  let font = FONT2;
53727
- let fade = 0;
53728
54154
  if (isOrient) {
53729
54155
  const growT = sizeT(boxW, boxH);
53730
54156
  const desiredFont = Math.round(
53731
54157
  FONT2 + growT * (REGION_FONT_MAX_ORIENT - FONT2)
53732
54158
  );
53733
54159
  const hasVal = chosen.valueLine !== void 0;
53734
- for (let f = desiredFont; f > FONT2; f--) {
54160
+ const extremesClean = (f) => {
54161
+ const halfW = measureLegendText(chosen.text, f) / 2;
54162
+ return [chosen.ax - halfW, chosen.ax + halfW].every((sx) => {
54163
+ const fAt = fillAt(sx, chosen.ay);
54164
+ return fAt === r.fill || isEmptyFill(fAt);
54165
+ });
54166
+ };
54167
+ let fit = -1;
54168
+ for (let f = desiredFont; f >= FONT2; f--) {
53735
54169
  if (stackW(chosen.text, chosen.valueLine, f) > boxW || stackH(hasVal, f) > boxH)
53736
54170
  continue;
53737
54171
  const gRect = regionLabelRect(
@@ -53748,14 +54182,18 @@ function layoutMap(resolvedIn, data, size, opts) {
53748
54182
  void 0,
53749
54183
  f
53750
54184
  );
53751
- if (!fitsRegions(gRect) || !fitsPois(gName)) continue;
53752
- const halfW = measureLegendText(chosen.text, f) / 2;
53753
- if (fillAt(chosen.ax - halfW, chosen.ay) !== r.fill || fillAt(chosen.ax + halfW, chosen.ay) !== r.fill)
54185
+ if (!fitsRegions(gRect) || !fitsPois(gName) || collides(gName))
53754
54186
  continue;
53755
- font = f;
54187
+ if (!extremesClean(f)) continue;
54188
+ fit = f;
53756
54189
  break;
53757
54190
  }
53758
- fade = font > FONT2 ? Math.round(growT * REGION_FADE_ORIENT) : 0;
54191
+ if (fit < 0) {
54192
+ if (!frameContainers.has(r.id)) continue;
54193
+ font = FONT2;
54194
+ } else {
54195
+ font = fit;
54196
+ }
53759
54197
  }
53760
54198
  const rRect = regionLabelRect(
53761
54199
  chosen.ax,
@@ -53773,8 +54211,9 @@ function layoutMap(resolvedIn, data, size, opts) {
53773
54211
  r.lineNumber,
53774
54212
  chosen.valueLine,
53775
54213
  font,
53776
- fade
54214
+ isOrient ? backdropLandTone : void 0
53777
54215
  );
54216
+ labeledRegionIds.add(r.id);
53778
54217
  regionLabelGuards.push({
53779
54218
  label: labels[labels.length - 1],
53780
54219
  rect: rRect
@@ -53792,6 +54231,7 @@ function layoutMap(resolvedIn, data, size, opts) {
53792
54231
  seed.lineNumber,
53793
54232
  valStr
53794
54233
  );
54234
+ labeledRegionIds.add(seed.iso);
53795
54235
  regionLabelGuards.push({
53796
54236
  label: labels[labels.length - 1],
53797
54237
  rect: regionLabelRect(seed.x, seed.y, text, valStr)
@@ -54223,13 +54663,30 @@ function layoutMap(resolvedIn, data, size, opts) {
54223
54663
  const nx = -dy / len;
54224
54664
  const ny = dx / len;
54225
54665
  const perp = labelH + 2;
54666
+ const pm = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
54667
+ lg.d
54668
+ );
54669
+ const pointAt = (t) => {
54670
+ if (pm?.[5] !== void 0) {
54671
+ const u = 1 - t;
54672
+ return [
54673
+ u * u * +pm[1] + 2 * u * t * +pm[5] + t * t * +pm[7],
54674
+ u * u * +pm[2] + 2 * u * t * +pm[6] + t * t * +pm[8]
54675
+ ];
54676
+ }
54677
+ if (pm?.[3] !== void 0)
54678
+ return [
54679
+ +pm[1] + (+pm[3] - +pm[1]) * t,
54680
+ +pm[2] + (+pm[4] - +pm[2]) * t
54681
+ ];
54682
+ return [a.cx + dx * t, a.cy + dy * t];
54683
+ };
54226
54684
  const candidates = [];
54227
54685
  for (const off of [0, perp, -perp, 2 * perp, -2 * perp])
54228
- for (const t of T_LIST)
54229
- candidates.push([
54230
- a.cx + dx * t + nx * off,
54231
- a.cy + dy * t + ny * off - 4
54232
- ]);
54686
+ for (const t of T_LIST) {
54687
+ const [bx, by] = pointAt(t);
54688
+ candidates.push([bx + nx * off, by + ny * off - 4]);
54689
+ }
54233
54690
  const clean = ([cx2, cy2]) => {
54234
54691
  const rect2 = { x: cx2 - w / 2, y: cy2 - labelH / 2, w, h: labelH };
54235
54692
  if (rect2.x < 0 || rect2.x + w > width || rect2.y < 0 || rect2.y + rect2.h > height)
@@ -54239,10 +54696,8 @@ function layoutMap(resolvedIn, data, size, opts) {
54239
54696
  if (placedEdge.some((o) => rectsOverlap(o, rect2))) return false;
54240
54697
  return true;
54241
54698
  };
54242
- const [cx, cy] = candidates.find(clean) ?? [
54243
- a.cx + dx * 0.5,
54244
- a.cy + dy * 0.5 - 4
54245
- ];
54699
+ const mid = pointAt(0.5);
54700
+ const [cx, cy] = candidates.find(clean) ?? [mid[0], mid[1] - 4];
54246
54701
  const style = labelOnFill(fillAt(cx, cy));
54247
54702
  legs[i] = {
54248
54703
  ...lg,
@@ -54270,7 +54725,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54270
54725
  const rawCountries = [];
54271
54726
  for (const f of worldLayer.values()) {
54272
54727
  const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
54273
- if (!iso || regionById.has(iso)) continue;
54728
+ if (!iso || regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54274
54729
  let hasReferencedSub = false;
54275
54730
  for (const k of regionById.keys())
54276
54731
  if (k.startsWith(iso + "-")) {
@@ -54344,7 +54799,7 @@ function layoutMap(resolvedIn, data, size, opts) {
54344
54799
  );
54345
54800
  if (usLayer && framedStateContainers) {
54346
54801
  for (const [iso, f] of usLayer) {
54347
- if (regionById.has(iso)) continue;
54802
+ if (regionById.has(iso) || labeledRegionIds.has(iso)) continue;
54348
54803
  const viewF = cullFeatureToView(f);
54349
54804
  if (!viewF) continue;
54350
54805
  const b = path.bounds(viewF);
@@ -54368,6 +54823,11 @@ function layoutMap(resolvedIn, data, size, opts) {
54368
54823
  waterBodies: data.waterBodies,
54369
54824
  countries: countryCandidates,
54370
54825
  palette,
54826
+ // One muted, contrast-floored tone shared with the orientation backdrop
54827
+ // (region pass) so a context country (Canada) and an orientation region
54828
+ // (Minnesota) read identically — never below the WCAG-AA floor, no fade.
54829
+ countryTone: backdropLandTone,
54830
+ waterTone: backdropWaterTone,
54371
54831
  project,
54372
54832
  collides,
54373
54833
  contentPoints,
@@ -54922,19 +55382,60 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
54922
55382
  sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
54923
55383
  };
54924
55384
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
54925
- layout.legs.forEach((leg, i) => {
54926
- const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
55385
+ const legEnd = (d) => {
55386
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
55387
+ d
55388
+ );
55389
+ if (!m) return null;
55390
+ const x0 = +m[1];
55391
+ const y0 = +m[2];
55392
+ let ex;
55393
+ let ey;
55394
+ let fromX;
55395
+ let fromY;
55396
+ if (m[3] !== void 0) {
55397
+ ex = +m[3];
55398
+ ey = +m[4];
55399
+ fromX = x0;
55400
+ fromY = y0;
55401
+ } else {
55402
+ ex = +m[7];
55403
+ ey = +m[8];
55404
+ fromX = +m[5];
55405
+ fromY = +m[6];
55406
+ }
55407
+ const dx = ex - fromX;
55408
+ const dy = ey - fromY;
55409
+ const len = Math.hypot(dx, dy);
55410
+ if (!(len > 0)) return null;
55411
+ return { ex, ey, tx: dx / len, ty: dy / len };
55412
+ };
55413
+ layout.legs.forEach((leg) => {
55414
+ const end = leg.arrow ? legEnd(leg.d) : null;
55415
+ let drawD = leg.d;
55416
+ let arrowTri = null;
55417
+ if (end) {
55418
+ const aLen = arrowSize(leg.width);
55419
+ const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1);
55420
+ const bx = end.ex - end.tx * aLen;
55421
+ const by = end.ey - end.ty * aLen;
55422
+ const px = -end.ty;
55423
+ const py = end.tx;
55424
+ arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
55425
+ drawD = leg.d.replace(
55426
+ /(-?[\d.]+),(-?[\d.]+)$/,
55427
+ `${bx.toFixed(2)},${by.toFixed(2)}`
55428
+ );
55429
+ }
55430
+ const p = gLegs.append("path").attr("d", drawD).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
54927
55431
  if (leg.tags) {
54928
55432
  for (const [group, value] of Object.entries(leg.tags)) {
54929
55433
  p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
54930
55434
  }
54931
55435
  }
54932
55436
  wireSync(p, leg.lineNumber);
54933
- if (leg.arrow) {
54934
- const id = nid(`dgmo-map-arrow-${i}`);
54935
- const s = arrowSize(leg.width);
54936
- defs.append("marker").attr("id", id).attr("viewBox", "0 0 10 10").attr("refX", 10).attr("refY", 5).attr("markerUnits", "userSpaceOnUse").attr("markerWidth", s).attr("markerHeight", s).attr("orient", "auto-start-reverse").append("path").attr("d", "M0,0L10,5L0,10z").attr("fill", leg.color);
54937
- p.attr("marker-end", `url(#${id})`);
55437
+ if (arrowTri) {
55438
+ gLegs.append("path").attr("d", arrowTri).attr("fill", leg.color).attr("stroke", leg.color).attr("stroke-width", 1).attr("stroke-linejoin", "round").attr("data-from-id", leg.fromId).attr("data-to-id", leg.toId);
54938
55439
  }
54939
55440
  if (leg.label !== void 0 && leg.labelX !== void 0) {
54940
55441
  const lt = emitText(
@@ -58563,9 +59064,9 @@ var init_renderer20 = __esm({
58563
59064
  }
58564
59065
  });
58565
59066
 
58566
- // node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
59067
+ // ../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
58567
59068
  var require_lz_string = __commonJS({
58568
- "node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports2, module2) {
59069
+ "../../diagrammo/dgmo/node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js"(exports2, module2) {
58569
59070
  "use strict";
58570
59071
  var LZString = (function() {
58571
59072
  var f = String.fromCharCode;
@@ -63998,7 +64499,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
63998
64499
 
63999
64500
  // src/auto/index.ts
64000
64501
  init_safe_href();
64001
- var VERSION = "0.33.0";
64502
+ var VERSION = "0.35.0";
64002
64503
  var DEFAULTS = {
64003
64504
  theme: "auto",
64004
64505
  palette: "slate",