@stacksjs/defaults 0.75.361 → 0.75.362

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 (208) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +2 -2
  3. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  4. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  5. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  7. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  8. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  9. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  10. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  11. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  12. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  16. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  17. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  18. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  19. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  20. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  21. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  22. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  24. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  27. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  28. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  29. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  32. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  37. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  38. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  39. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  40. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  42. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  43. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  56. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  57. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  58. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  59. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  69. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  70. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  111. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  120. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  121. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  122. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  124. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  125. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  126. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  127. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  128. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  129. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  130. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  134. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  141. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  147. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  149. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  175. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  186. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  187. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  188. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  189. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  190. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  191. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  192. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  194. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  195. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  196. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  199. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  201. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  205. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  206. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  207. package/ide/vscode/package.json +1 -1
  208. package/package.json +2 -2
@@ -0,0 +1,670 @@
1
+ import { recordDiagnostic } from './diagnostics.mjs';
2
+ import {
3
+ asArray,
4
+ isFinitePoint,
5
+ normalizeRoutePoints,
6
+ properSegmentIntersection,
7
+ segmentIntersectsRect,
8
+ } from './geometry.mjs';
9
+
10
+ const DEFAULTS = Object.freeze({
11
+ clearance: 2,
12
+ minimumDetourRatio: 2.5,
13
+ minimumExcessLengthPx: 200,
14
+ minimumEmptyExcursionPx: 96,
15
+ maximumObstacleCount: 80,
16
+ sharedCorridorMinimumPx: 32,
17
+ });
18
+
19
+ const OUTWARD = Object.freeze({
20
+ left: [-1, 0],
21
+ right: [1, 0],
22
+ top: [0, -1],
23
+ bottom: [0, 1],
24
+ });
25
+
26
+ function rounded(value) {
27
+ return Math.round(value * 100) / 100;
28
+ }
29
+
30
+ function pointKey(point) {
31
+ return `${point[0]}\u0000${point[1]}`;
32
+ }
33
+
34
+ class MinHeap {
35
+ constructor() {
36
+ this.entries = [];
37
+ }
38
+
39
+ push(key, distance) {
40
+ const entry = { key, distance };
41
+ this.entries.push(entry);
42
+ let index = this.entries.length - 1;
43
+ while (index > 0) {
44
+ const parent = Math.floor((index - 1) / 2);
45
+ if (this.entries[parent].distance <= distance) break;
46
+ this.entries[index] = this.entries[parent];
47
+ index = parent;
48
+ }
49
+ this.entries[index] = entry;
50
+ }
51
+
52
+ pop() {
53
+ if (!this.entries.length) return null;
54
+ const first = this.entries[0];
55
+ const last = this.entries.pop();
56
+ if (!this.entries.length) return first;
57
+ let index = 0;
58
+ while (true) {
59
+ const left = index * 2 + 1;
60
+ const right = left + 1;
61
+ if (left >= this.entries.length) break;
62
+ const child = right < this.entries.length
63
+ && this.entries[right].distance < this.entries[left].distance ? right : left;
64
+ if (this.entries[child].distance >= last.distance) break;
65
+ this.entries[index] = this.entries[child];
66
+ index = child;
67
+ }
68
+ this.entries[index] = last;
69
+ return first;
70
+ }
71
+ }
72
+
73
+ function orthogonalLength(points) {
74
+ let total = 0;
75
+ for (let index = 0; index < points.length - 1; index += 1) {
76
+ const [x1, y1] = points[index];
77
+ const [x2, y2] = points[index + 1];
78
+ if (x1 !== x2 && y1 !== y2) return null;
79
+ total += Math.abs(x2 - x1) + Math.abs(y2 - y1);
80
+ }
81
+ return total;
82
+ }
83
+
84
+ function inferredSide(points, endpoint) {
85
+ if (points.length < 2) return null;
86
+ const start = endpoint === 'source' ? points[0] : points.at(-2);
87
+ const end = endpoint === 'source' ? points[1] : points.at(-1);
88
+ const dx = end[0] - start[0];
89
+ const dy = end[1] - start[1];
90
+ if (endpoint === 'source') {
91
+ if (dx > 0 && dy === 0) return 'right';
92
+ if (dx < 0 && dy === 0) return 'left';
93
+ if (dy > 0 && dx === 0) return 'bottom';
94
+ if (dy < 0 && dx === 0) return 'top';
95
+ } else {
96
+ if (dx > 0 && dy === 0) return 'left';
97
+ if (dx < 0 && dy === 0) return 'right';
98
+ if (dy > 0 && dx === 0) return 'top';
99
+ if (dy < 0 && dx === 0) return 'bottom';
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function moveOutward(point, side, distance) {
105
+ const [dx, dy] = OUTWARD[side] || [0, 0];
106
+ return [point[0] + dx * distance, point[1] + dy * distance];
107
+ }
108
+
109
+ function expandedRect(rect, clearance) {
110
+ return {
111
+ id: rect.id,
112
+ x: rect.x - clearance,
113
+ y: rect.y - clearance,
114
+ width: rect.width + clearance * 2,
115
+ height: rect.height + clearance * 2,
116
+ };
117
+ }
118
+
119
+ function boundsForRects(rects) {
120
+ const usable = [...rects].filter((rect) => (
121
+ rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height)
122
+ && rect.width >= 0 && rect.height >= 0
123
+ ));
124
+ if (!usable.length) return null;
125
+ const left = Math.min(...usable.map((rect) => rect.x));
126
+ const top = Math.min(...usable.map((rect) => rect.y));
127
+ const right = Math.max(...usable.map((rect) => rect.x + rect.width));
128
+ const bottom = Math.max(...usable.map((rect) => rect.y + rect.height));
129
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
130
+ }
131
+
132
+ function boundsForPoints(points) {
133
+ if (!points.length) return null;
134
+ const xs = points.map(([x]) => x);
135
+ const ys = points.map(([, y]) => y);
136
+ const left = Math.min(...xs);
137
+ const top = Math.min(...ys);
138
+ const right = Math.max(...xs);
139
+ const bottom = Math.max(...ys);
140
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
141
+ }
142
+
143
+ function outsideExcursion(routeBounds, contentBounds) {
144
+ if (!routeBounds || !contentBounds) return null;
145
+ const sides = {
146
+ left: Math.max(0, contentBounds.left - routeBounds.left),
147
+ top: Math.max(0, contentBounds.top - routeBounds.top),
148
+ right: Math.max(0, routeBounds.right - contentBounds.right),
149
+ bottom: Math.max(0, routeBounds.bottom - contentBounds.bottom),
150
+ };
151
+ return { ...sides, maximum: Math.max(...Object.values(sides)) };
152
+ }
153
+
154
+ function pointDistanceFromRect(point, rect) {
155
+ const dx = Math.max(rect.x - point[0], 0, point[0] - (rect.x + rect.width));
156
+ const dy = Math.max(rect.y - point[1], 0, point[1] - (rect.y + rect.height));
157
+ return dx + dy;
158
+ }
159
+
160
+ function emptyControlPointClearance(points, contentRects) {
161
+ const controls = points.slice(1, -1);
162
+ const rects = [...contentRects].filter((rect) => (
163
+ rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height)
164
+ && rect.width >= 0 && rect.height >= 0
165
+ ));
166
+ if (!controls.length || !rects.length) return null;
167
+ const distances = controls.map((point) => Math.min(
168
+ ...rects.map((rect) => pointDistanceFromRect(point, rect)),
169
+ ));
170
+ const maximum = Math.max(...distances);
171
+ return { maximum, point: controls[distances.indexOf(maximum)] };
172
+ }
173
+
174
+ function pointBlocked(point, obstacles) {
175
+ return obstacles.some((rect) => (
176
+ point[0] >= rect.x && point[0] <= rect.x + rect.width
177
+ && point[1] >= rect.y && point[1] <= rect.y + rect.height
178
+ ));
179
+ }
180
+
181
+ // Prune only against the candidate segment, never the route's initial bounds:
182
+ // a later detour can still encounter any obstacle in the complete inventory.
183
+ // Keep epsilon-near bounds for the existing exact predicates to decide.
184
+ function segmentBlocked(start, end, obstacles) {
185
+ const minX = Math.min(start[0], end[0]);
186
+ const maxX = Math.max(start[0], end[0]);
187
+ const minY = Math.min(start[1], end[1]);
188
+ const maxY = Math.max(start[1], end[1]);
189
+ return obstacles.some((rect) => {
190
+ if (maxX < Math.min(rect.x, rect.x + rect.width) - 0.0001
191
+ || minX > Math.max(rect.x, rect.x + rect.width) + 0.0001
192
+ || maxY < Math.min(rect.y, rect.y + rect.height) - 0.0001
193
+ || minY > Math.max(rect.y, rect.y + rect.height) + 0.0001) return false;
194
+ return segmentIntersectsRect({ start, end }, rect);
195
+ });
196
+ }
197
+
198
+ function segmentConflictsWithAvoided(start, end, avoidedSegments, minimumOverlapPx, allowCrossings) {
199
+ const minX = Math.min(start[0], end[0]);
200
+ const maxX = Math.max(start[0], end[0]);
201
+ const minY = Math.min(start[1], end[1]);
202
+ const maxY = Math.max(start[1], end[1]);
203
+ return avoidedSegments.some((segment) => {
204
+ // A zero overlap threshold also rejects disjoint segments (0 >= 0).
205
+ if (minimumOverlapPx > 0 && (maxX < Math.min(segment.start[0], segment.end[0]) - 0.0001
206
+ || minX > Math.max(segment.start[0], segment.end[0]) + 0.0001
207
+ || maxY < Math.min(segment.start[1], segment.end[1]) - 0.0001
208
+ || minY > Math.max(segment.start[1], segment.end[1]) + 0.0001)) return false;
209
+ return (!allowCrossings && (properSegmentIntersection(start, end, segment.start, segment.end)
210
+ || orthogonalTouchOnAvoidedInterior(start, end, segment.start, segment.end)))
211
+ || collinearOverlap(start, end, segment.start, segment.end) >= minimumOverlapPx;
212
+ });
213
+ }
214
+
215
+ function orthogonalTouchOnAvoidedInterior(start, end, avoidedStart, avoidedEnd) {
216
+ const epsilon = 0.0001;
217
+ const candidateHorizontal = Math.abs(start[1] - end[1]) <= epsilon;
218
+ const candidateVertical = Math.abs(start[0] - end[0]) <= epsilon;
219
+ const avoidedHorizontal = Math.abs(avoidedStart[1] - avoidedEnd[1]) <= epsilon;
220
+ const avoidedVertical = Math.abs(avoidedStart[0] - avoidedEnd[0]) <= epsilon;
221
+ if (candidateHorizontal && avoidedVertical) {
222
+ const x = avoidedStart[0];
223
+ const y = start[1];
224
+ return x >= Math.min(start[0], end[0]) - epsilon
225
+ && x <= Math.max(start[0], end[0]) + epsilon
226
+ && y > Math.min(avoidedStart[1], avoidedEnd[1]) + epsilon
227
+ && y < Math.max(avoidedStart[1], avoidedEnd[1]) - epsilon;
228
+ }
229
+ if (candidateVertical && avoidedHorizontal) {
230
+ const x = start[0];
231
+ const y = avoidedStart[1];
232
+ return y >= Math.min(start[1], end[1]) - epsilon
233
+ && y <= Math.max(start[1], end[1]) + epsilon
234
+ && x > Math.min(avoidedStart[0], avoidedEnd[0]) + epsilon
235
+ && x < Math.max(avoidedStart[0], avoidedEnd[0]) - epsilon;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ function pointOnSegmentInterior(point, start, end) {
241
+ const epsilon = 0.0001;
242
+ const cross = (end[0] - start[0]) * (point[1] - start[1])
243
+ - (end[1] - start[1]) * (point[0] - start[0]);
244
+ if (Math.abs(cross) > epsilon) return false;
245
+ const dot = (point[0] - start[0]) * (point[0] - end[0])
246
+ + (point[1] - start[1]) * (point[1] - end[1]);
247
+ return dot < -epsilon;
248
+ }
249
+
250
+ function pointOnAvoidedInterior(point, avoidedSegments) {
251
+ return avoidedSegments.some((segment) => (
252
+ pointOnSegmentInterior(point, segment.start, segment.end)
253
+ ));
254
+ }
255
+
256
+ function writeGridMetrics(metrics, patch) {
257
+ if (!metrics || typeof metrics !== 'object') return;
258
+ Object.assign(metrics, patch);
259
+ }
260
+
261
+ export function shortestOrthogonalGridRoute({
262
+ start,
263
+ end,
264
+ points,
265
+ obstacles,
266
+ fromSide,
267
+ toSide,
268
+ clearance,
269
+ maximumObstacleCount,
270
+ endpointStubPx = clearance + 2,
271
+ maximumGridNodes = Infinity,
272
+ avoidedSegments = [],
273
+ allowAvoidedCrossings = false,
274
+ minimumAvoidedOverlapPx = 8,
275
+ routeSeparationPx = 8,
276
+ minimumSegmentPx = 8,
277
+ borderSegments = [],
278
+ bendPenaltyPx = 0,
279
+ accept = () => true,
280
+ metrics,
281
+ }) {
282
+ writeGridMetrics(metrics, {
283
+ status: 'initializing',
284
+ maximumGridNodes,
285
+ obstacleCount: 0,
286
+ avoidedSegmentCount: 0,
287
+ coordinateCount: 0,
288
+ candidateNodeCount: 0,
289
+ usableNodeCount: 0,
290
+ graphEdgeCount: 0,
291
+ visitedNodeCount: 0,
292
+ attemptCount: 0,
293
+ });
294
+ if (!OUTWARD[fromSide] || !OUTWARD[toSide]) {
295
+ writeGridMetrics(metrics, { status: 'unsupported-endpoint-side' });
296
+ return null;
297
+ }
298
+ const startStub = moveOutward(start, fromSide, endpointStubPx);
299
+ const endStub = moveOutward(end, toSide, endpointStubPx);
300
+ // The graph may legally leave the initial endpoint bounds to find a clear
301
+ // corridor. Keep every bounded obstacle and occupied relationship visible
302
+ // to that search; filtering them against the initial box lets a detour walk
303
+ // straight through geometry that only becomes relevant after it leaves the
304
+ // box. The explicit obstacle/node budgets below keep this deterministic.
305
+ const expanded = [...obstacles]
306
+ .filter((rect) => rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height))
307
+ .map((rect) => expandedRect(rect, clearance));
308
+ const relevantAvoidedSegments = [...avoidedSegments]
309
+ .filter((segment) => segment?.start && segment?.end);
310
+ // Frame borders may be crossed perpendicularly but never borrowed as a
311
+ // corridor: the composition gate rejects any collinear run along them.
312
+ const relevantBorderSegments = [...borderSegments]
313
+ .filter((segment) => segment?.start && segment?.end);
314
+ writeGridMetrics(metrics, {
315
+ obstacleCount: expanded.length,
316
+ avoidedSegmentCount: relevantAvoidedSegments.length,
317
+ });
318
+ if (expanded.length > maximumObstacleCount) {
319
+ writeGridMetrics(metrics, { status: 'obstacle-budget-exceeded' });
320
+ return null;
321
+ }
322
+
323
+ const xs = new Set([startStub[0], endStub[0], ...points.map(([x]) => x)]);
324
+ const ys = new Set([startStub[1], endStub[1], ...points.map(([, y]) => y)]);
325
+ for (const rect of expanded) {
326
+ xs.add(rect.x - 1);
327
+ xs.add(rect.x + rect.width + 1);
328
+ ys.add(rect.y - 1);
329
+ ys.add(rect.y + rect.height + 1);
330
+ }
331
+ for (const segment of relevantAvoidedSegments) {
332
+ const [segmentStart, segmentEnd] = [segment.start, segment.end];
333
+ xs.add(segmentStart[0]);
334
+ xs.add(segmentEnd[0]);
335
+ ys.add(segmentStart[1]);
336
+ ys.add(segmentEnd[1]);
337
+ if (Math.abs(segmentStart[0] - segmentEnd[0]) <= 0.0001) {
338
+ xs.add(segmentStart[0] - routeSeparationPx);
339
+ xs.add(segmentStart[0] + routeSeparationPx);
340
+ }
341
+ if (Math.abs(segmentStart[1] - segmentEnd[1]) <= 0.0001) {
342
+ ys.add(segmentStart[1] - routeSeparationPx);
343
+ ys.add(segmentStart[1] + routeSeparationPx);
344
+ }
345
+ }
346
+ function addBorderCoordinates(distance) {
347
+ for (const segment of relevantBorderSegments) {
348
+ if (Math.abs(segment.start[0] - segment.end[0]) <= 0.0001) {
349
+ xs.add(segment.start[0] - distance);
350
+ xs.add(segment.start[0] + distance);
351
+ }
352
+ if (Math.abs(segment.start[1] - segment.end[1]) <= 0.0001) {
353
+ ys.add(segment.start[1] - distance);
354
+ ys.add(segment.start[1] + distance);
355
+ }
356
+ }
357
+ }
358
+ addBorderCoordinates(routeSeparationPx);
359
+ // Grid lines closer than a readable segment would let the search emit a
360
+ // micro jog between two obstacle edges; keep the endpoint stubs and coalesce
361
+ // the rest so every turn the route can take is at least one segment long.
362
+ const coalesce = (values, keep, fillGaps) => {
363
+ const sorted = values.sort((a, b) => a - b);
364
+ if (!fillGaps) return sorted.filter((value, index) => (
365
+ index === 0 || keep.has(value) || value - sorted[index - 1] >= minimumSegmentPx
366
+ ));
367
+ // A dense chain of discarded coordinates can erase a usable corridor.
368
+ // Compare with the last retained line while reserving readable spacing
369
+ // around the endpoint lines that must be kept.
370
+ return sorted.reduce((retained, value) => {
371
+ if (keep.has(value) || ((!retained.length || value - retained.at(-1) >= minimumSegmentPx)
372
+ && [...keep].every(endpoint => Math.abs(value - endpoint) >= minimumSegmentPx))) retained.push(value);
373
+ return retained;
374
+ }, []);
375
+ };
376
+ // Keep accepted routes stable. A failed or caller-rejected search can use
377
+ // the denser coordinate set, within the same original node budget.
378
+ let remainingGridNodes = maximumGridNodes;
379
+ for (let attempt = 0; attempt < 2; attempt += 1) {
380
+ // A pinned endpoint stub may land on a frame border. Nearby candidates
381
+ // closer than the minimum segment disappear during coalescing. Recovery
382
+ // adds a full-segment corridor without changing the attempt or node budget.
383
+ if (attempt > 0 && routeSeparationPx < minimumSegmentPx) addBorderCoordinates(minimumSegmentPx);
384
+ const orderedX = coalesce([...xs], new Set([startStub[0], endStub[0]]), attempt > 0);
385
+ const orderedY = coalesce([...ys], new Set([startStub[1], endStub[1]]), attempt > 0);
386
+ const candidateNodeCount = orderedX.length * orderedY.length;
387
+ writeGridMetrics(metrics, {
388
+ coordinateCount: (metrics?.coordinateCount || 0) + orderedX.length + orderedY.length,
389
+ candidateNodeCount: (metrics?.candidateNodeCount || 0) + candidateNodeCount,
390
+ attemptCount: attempt + 1,
391
+ });
392
+ if (candidateNodeCount > remainingGridNodes) {
393
+ writeGridMetrics(metrics, { status: 'node-budget-exceeded' });
394
+ return null;
395
+ }
396
+ remainingGridNodes -= candidateNodeCount;
397
+ const nodes = new Map();
398
+ for (const x of orderedX) {
399
+ for (const y of orderedY) {
400
+ const point = [x, y];
401
+ if (!pointBlocked(point, expanded)
402
+ && (allowAvoidedCrossings || !pointOnAvoidedInterior(point, relevantAvoidedSegments))) {
403
+ nodes.set(pointKey(point), point);
404
+ }
405
+ }
406
+ }
407
+ writeGridMetrics(metrics, { usableNodeCount: (metrics?.usableNodeCount || 0) + nodes.size });
408
+ if (!nodes.has(pointKey(startStub)) || !nodes.has(pointKey(endStub))) {
409
+ writeGridMetrics(metrics, { status: 'endpoint-blocked' });
410
+ return null;
411
+ }
412
+
413
+ const adjacency = new Map([...nodes.keys()].map((key) => [key, []]));
414
+ let graphEdgeCount = 0;
415
+ const connectLine = (line, axis) => {
416
+ for (let index = 0; index < line.length - 1; index += 1) {
417
+ const left = line[index];
418
+ const right = line[index + 1];
419
+ if (segmentBlocked(left, right, expanded)) continue;
420
+ if (segmentConflictsWithAvoided(
421
+ left,
422
+ right,
423
+ relevantAvoidedSegments,
424
+ minimumAvoidedOverlapPx,
425
+ allowAvoidedCrossings,
426
+ )) continue;
427
+ if (relevantBorderSegments.some((segment) => (
428
+ collinearOverlap(left, right, segment.start, segment.end) > 0.0001
429
+ ))) continue;
430
+ const distance = Math.abs(right[0] - left[0]) + Math.abs(right[1] - left[1]);
431
+ const leftKey = pointKey(left);
432
+ const rightKey = pointKey(right);
433
+ adjacency.get(leftKey).push([rightKey, distance, axis === 'h' ? 'R' : 'D']);
434
+ adjacency.get(rightKey).push([leftKey, distance, axis === 'h' ? 'L' : 'U']);
435
+ graphEdgeCount += 1;
436
+ }
437
+ };
438
+ for (const y of orderedY) {
439
+ connectLine(orderedX.map((x) => nodes.get(pointKey([x, y]))).filter(Boolean), 'h');
440
+ }
441
+ for (const x of orderedX) {
442
+ connectLine(orderedY.map((y) => nodes.get(pointKey([x, y]))).filter(Boolean), 'v');
443
+ }
444
+ writeGridMetrics(metrics, { graphEdgeCount: (metrics?.graphEdgeCount || 0) + graphEdgeCount });
445
+
446
+ // The search state carries the incoming direction so a turn can cost extra
447
+ // and a reversal is never taken: the pure shortest path hugs every obstacle
448
+ // corner with a staircase of short jogs, while a bend-penalised one takes
449
+ // the same corridor in a few long strokes. The first stub already leaves
450
+ // the endpoint along its side and the last one arrives along the end side.
451
+ const directionOf = ([dx, dy]) => (dx > 0 ? 'R' : dx < 0 ? 'L' : dy > 0 ? 'D' : 'U');
452
+ const opposite = { R: 'L', L: 'R', D: 'U', U: 'D' };
453
+ const stateKey = (key, direction) => `${key}|${direction}`;
454
+ const sourceAxis = directionOf(OUTWARD[fromSide]);
455
+ const targetAxis = opposite[directionOf(OUTWARD[toSide])];
456
+ const source = pointKey(startStub);
457
+ const target = pointKey(endStub);
458
+ const sourceState = stateKey(source, sourceAxis);
459
+ const distances = new Map([[sourceState, 0]]);
460
+ const previous = new Map();
461
+ const queue = new MinHeap();
462
+ queue.push(sourceState, 0);
463
+ let visitedNodeCount = 0;
464
+ let targetState = null;
465
+ while (queue.entries.length) {
466
+ const next = queue.pop();
467
+ const current = next.key;
468
+ const currentDistance = next.distance;
469
+ if (currentDistance !== distances.get(current)) continue;
470
+ visitedNodeCount += 1;
471
+ const [currentNode, currentAxis] = current.split('|');
472
+ if (currentNode === target) {
473
+ // The end stub is outside the graph: never reverse back onto it.
474
+ if (currentAxis === opposite[targetAxis]) continue;
475
+ // Arriving on the wrong axis costs one final turn onto the end stub.
476
+ const arrival = currentDistance + (currentAxis === targetAxis ? 0 : bendPenaltyPx);
477
+ if (targetState == null || arrival < targetState.distance) {
478
+ targetState = { key: current, distance: arrival };
479
+ }
480
+ if (currentAxis === targetAxis || bendPenaltyPx === 0) break;
481
+ continue;
482
+ }
483
+ if (targetState && currentDistance >= targetState.distance) break;
484
+ for (const [neighbor, weight, axis] of adjacency.get(currentNode) || []) {
485
+ if (axis === opposite[currentAxis]) continue;
486
+ const candidate = currentDistance + weight + (axis === currentAxis ? 0 : bendPenaltyPx);
487
+ const neighborState = stateKey(neighbor, axis);
488
+ if (candidate >= (distances.get(neighborState) ?? Infinity)) continue;
489
+ distances.set(neighborState, candidate);
490
+ previous.set(neighborState, current);
491
+ queue.push(neighborState, candidate);
492
+ }
493
+ }
494
+ writeGridMetrics(metrics, { visitedNodeCount: (metrics?.visitedNodeCount || 0) + visitedNodeCount });
495
+ if (!targetState) {
496
+ writeGridMetrics(metrics, { status: 'no-route' });
497
+ continue;
498
+ }
499
+ const reversed = [];
500
+ for (let key = targetState.key; key; key = previous.get(key)) {
501
+ reversed.push(nodes.get(key.split('|')[0]));
502
+ if (key === sourceState) break;
503
+ }
504
+ if (pointKey(reversed.at(-1)) !== source) {
505
+ writeGridMetrics(metrics, { status: 'broken-predecessor-chain' });
506
+ return null;
507
+ }
508
+ const shortestPoints = normalizeRoutePoints([start, ...reversed.reverse(), end]);
509
+ if (!accept(shortestPoints)) {
510
+ writeGridMetrics(metrics, { status: 'rejected' });
511
+ continue;
512
+ }
513
+ writeGridMetrics(metrics, { status: 'routed' });
514
+ return {
515
+ points: shortestPoints,
516
+ length: orthogonalLength(shortestPoints),
517
+ obstacleCount: expanded.length,
518
+ };
519
+ }
520
+ return null;
521
+ }
522
+
523
+ function collinearOverlap(leftStart, leftEnd, rightStart, rightEnd) {
524
+ if (leftStart[0] === leftEnd[0] && rightStart[0] === rightEnd[0]
525
+ && leftStart[0] === rightStart[0]) {
526
+ return Math.max(0, Math.min(Math.max(leftStart[1], leftEnd[1]), Math.max(rightStart[1], rightEnd[1]))
527
+ - Math.max(Math.min(leftStart[1], leftEnd[1]), Math.min(rightStart[1], rightEnd[1])));
528
+ }
529
+ if (leftStart[1] === leftEnd[1] && rightStart[1] === rightEnd[1]
530
+ && leftStart[1] === rightStart[1]) {
531
+ return Math.max(0, Math.min(Math.max(leftStart[0], leftEnd[0]), Math.max(rightStart[0], rightEnd[0]))
532
+ - Math.max(Math.min(leftStart[0], leftEnd[0]), Math.min(rightStart[0], rightEnd[0])));
533
+ }
534
+ return 0;
535
+ }
536
+
537
+ function segmentOutsideContent(start, end, contentBounds) {
538
+ if (!contentBounds) return false;
539
+ const midpoint = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
540
+ return midpoint[0] < contentBounds.left || midpoint[0] > contentBounds.right
541
+ || midpoint[1] < contentBounds.top || midpoint[1] > contentBounds.bottom;
542
+ }
543
+
544
+ function sharesOuterCorridor({ relation, relations, pathFor, points, contentBounds, minimumOverlap }) {
545
+ for (const other of asArray(relations)) {
546
+ if (!other || other === relation) continue;
547
+ const related = relation.from === other.from || relation.from === other.to
548
+ || relation.to === other.from || relation.to === other.to;
549
+ if (!related) continue;
550
+ const otherPoints = normalizeRoutePoints(pathFor(other)?.points || []);
551
+ for (let left = 0; left < points.length - 1; left += 1) {
552
+ if (!segmentOutsideContent(points[left], points[left + 1], contentBounds)) continue;
553
+ for (let right = 0; right < otherPoints.length - 1; right += 1) {
554
+ if (collinearOverlap(points[left], points[left + 1], otherPoints[right], otherPoints[right + 1]) >= minimumOverlap) {
555
+ return true;
556
+ }
557
+ }
558
+ }
559
+ }
560
+ return false;
561
+ }
562
+
563
+ function relationshipSubject(diagramType, relationCollection, relationIndex, relation) {
564
+ return {
565
+ diagramType,
566
+ collection: relationCollection,
567
+ index: relationIndex,
568
+ ...(relation.id ? { id: relation.id } : {}),
569
+ from: relation.from,
570
+ to: relation.to,
571
+ };
572
+ }
573
+
574
+ /**
575
+ * Reject conspicuous authored detours without penalizing routes whose length is
576
+ * explained by opaque-node avoidance or a related shared outer corridor.
577
+ */
578
+ export function cleanRouteDetourProblems({
579
+ relations,
580
+ obstacles,
581
+ contentRects = obstacles,
582
+ endpointIds,
583
+ pathFor,
584
+ fromSideFor,
585
+ toSideFor,
586
+ diagramType,
587
+ relationCollection,
588
+ profile,
589
+ thresholds = {},
590
+ }) {
591
+ if (profile !== 'showcase') return [];
592
+ const policy = { ...DEFAULTS, ...thresholds };
593
+ const obstacleList = [...obstacles];
594
+ const contentBounds = boundsForRects(contentRects);
595
+ const problems = [];
596
+ for (const [relationIndex, relation] of asArray(relations).entries()) {
597
+ if (!relation || !endpointIds?.has(relation.from) || !endpointIds?.has(relation.to)) continue;
598
+ if (!Array.isArray(relation.via) || relation.via.length === 0) continue;
599
+ const points = normalizeRoutePoints(pathFor(relation)?.points || []);
600
+ if (points.length < 3 || !points.every((point) => Array.isArray(point) && isFinitePoint(...point))) continue;
601
+ const actualLength = orthogonalLength(points);
602
+ if (!Number.isFinite(actualLength)) continue;
603
+ const start = points[0];
604
+ const end = points.at(-1);
605
+ const manhattan = Math.abs(end[0] - start[0]) + Math.abs(end[1] - start[1]);
606
+ if (actualLength < manhattan * policy.minimumDetourRatio
607
+ || actualLength - manhattan < policy.minimumExcessLengthPx) continue;
608
+ const routeBounds = boundsForPoints(points);
609
+ const excursion = outsideExcursion(routeBounds, contentBounds);
610
+ const emptyClearance = emptyControlPointClearance(points, obstacleList);
611
+ if (Math.max(excursion?.maximum || 0, emptyClearance?.maximum || 0)
612
+ < policy.minimumEmptyExcursionPx) continue;
613
+ if (sharesOuterCorridor({
614
+ relation,
615
+ relations,
616
+ pathFor,
617
+ points,
618
+ contentBounds,
619
+ minimumOverlap: policy.sharedCorridorMinimumPx,
620
+ })) continue;
621
+
622
+ const fromSide = fromSideFor?.(relation) || inferredSide(points, 'source');
623
+ const toSide = toSideFor?.(relation) || inferredSide(points, 'target');
624
+ const shortest = shortestOrthogonalGridRoute({
625
+ start,
626
+ end,
627
+ points,
628
+ obstacles: obstacleList,
629
+ fromSide,
630
+ toSide,
631
+ clearance: policy.clearance,
632
+ maximumObstacleCount: policy.maximumObstacleCount,
633
+ });
634
+ if (!shortest || !Number.isFinite(shortest.length) || shortest.length <= 0) continue;
635
+ const detourRatio = actualLength / shortest.length;
636
+ const excessLength = actualLength - shortest.length;
637
+ if (detourRatio < policy.minimumDetourRatio || excessLength < policy.minimumExcessLengthPx) continue;
638
+
639
+ const relationId = relation.id ? ` id "${relation.id}"` : '';
640
+ const message = `[composition/excessive-route-detour] ${diagramType} ${relationCollection}[${relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" travels ${Math.round(actualLength)}px, ${rounded(detourRatio)}x the ${Math.round(shortest.length)}px shortest obstacle-clearing orthogonal route, and reaches ${Math.round(excursion.maximum)}px beyond the content bounds — remove the distant via corridor or move it close to the connected content.`;
641
+ const supportedFix = 'remove the distant via points and retry automatic routing, or keep the endpoint sides and move the via corridor near the connected nodes while preserving labels and direction';
642
+ recordDiagnostic({
643
+ code: 'composition/excessive-route-detour',
644
+ severity: 'error',
645
+ message,
646
+ subject: relationshipSubject(diagramType, relationCollection, relationIndex, relation),
647
+ evidence: {
648
+ points,
649
+ actualLengthPx: rounded(actualLength),
650
+ shortestLegalPoints: shortest.points,
651
+ shortestLegalLengthPx: rounded(shortest.length),
652
+ detourRatio: rounded(detourRatio),
653
+ excessLengthPx: rounded(excessLength),
654
+ routeBounds,
655
+ contentBounds,
656
+ emptyExcursionPx: excursion,
657
+ emptyControlPointClearancePx: emptyClearance,
658
+ obstacleCount: shortest.obstacleCount,
659
+ thresholds: {
660
+ minimumDetourRatio: policy.minimumDetourRatio,
661
+ minimumExcessLengthPx: policy.minimumExcessLengthPx,
662
+ minimumEmptyExcursionPx: policy.minimumEmptyExcursionPx,
663
+ },
664
+ },
665
+ supportedFixes: [supportedFix],
666
+ });
667
+ problems.push(message);
668
+ }
669
+ return problems;
670
+ }