@misoto22/design 0.6.1 → 0.7.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 (157) hide show
  1. package/README.md +12 -0
  2. package/dist/agent/Accordion.md +1 -1
  3. package/dist/agent/Alert.md +1 -1
  4. package/dist/agent/AppShell.md +1 -1
  5. package/dist/agent/ArchitectureFigure.md +48 -0
  6. package/dist/agent/AreaChart.md +1 -1
  7. package/dist/agent/Article.md +1 -1
  8. package/dist/agent/Avatar.md +1 -1
  9. package/dist/agent/Badge.md +1 -1
  10. package/dist/agent/BarChart.md +1 -1
  11. package/dist/agent/BarList.md +1 -1
  12. package/dist/agent/BigNumber.md +1 -1
  13. package/dist/agent/BoxPlot.md +1 -1
  14. package/dist/agent/Breadcrumb.md +1 -1
  15. package/dist/agent/BulletChart.md +1 -1
  16. package/dist/agent/Button.md +1 -1
  17. package/dist/agent/Calendar.md +1 -1
  18. package/dist/agent/Card.md +1 -1
  19. package/dist/agent/Checkbox.md +1 -1
  20. package/dist/agent/Collapsible.md +1 -1
  21. package/dist/agent/Combobox.md +1 -1
  22. package/dist/agent/Command.md +1 -1
  23. package/dist/agent/ComposedChart.md +1 -1
  24. package/dist/agent/ContextMenu.md +1 -1
  25. package/dist/agent/DataflowFigure.md +27 -0
  26. package/dist/agent/DatePicker.md +1 -1
  27. package/dist/agent/Diagram.md +1 -1
  28. package/dist/agent/DiagramCanvas.md +47 -0
  29. package/dist/agent/DiagramExportMenu.md +38 -0
  30. package/dist/agent/DiagramInspector.md +50 -0
  31. package/dist/agent/DiagramLegend.md +34 -0
  32. package/dist/agent/DiagramMinimap.md +48 -0
  33. package/dist/agent/DiagramToolbar.md +47 -0
  34. package/dist/agent/Dialog.md +1 -1
  35. package/dist/agent/DropdownMenu.md +1 -1
  36. package/dist/agent/EmptyState.md +1 -1
  37. package/dist/agent/ErrorState.md +1 -1
  38. package/dist/agent/Facet.md +1 -1
  39. package/dist/agent/Field.md +1 -1
  40. package/dist/agent/FigureBand.md +1 -1
  41. package/dist/agent/FloatingIconButton.md +1 -1
  42. package/dist/agent/FunnelChart.md +1 -1
  43. package/dist/agent/Heatmap.md +1 -1
  44. package/dist/agent/Histogram.md +1 -1
  45. package/dist/agent/Input.md +1 -1
  46. package/dist/agent/Kbd.md +1 -1
  47. package/dist/agent/LifecycleFigure.md +28 -0
  48. package/dist/agent/LineChart.md +1 -1
  49. package/dist/agent/LinkArrow.md +1 -1
  50. package/dist/agent/NativeSelect.md +1 -1
  51. package/dist/agent/NavItem.md +1 -1
  52. package/dist/agent/Pagination.md +1 -1
  53. package/dist/agent/PieChart.md +1 -1
  54. package/dist/agent/Popover.md +1 -1
  55. package/dist/agent/Progress.md +1 -1
  56. package/dist/agent/RadarChart.md +1 -1
  57. package/dist/agent/RadialChart.md +1 -1
  58. package/dist/agent/RadioGroup.md +1 -1
  59. package/dist/agent/SankeyChart.md +1 -1
  60. package/dist/agent/ScatterChart.md +1 -1
  61. package/dist/agent/ScrollArea.md +1 -1
  62. package/dist/agent/SearchableMenu.md +1 -1
  63. package/dist/agent/Select.md +1 -1
  64. package/dist/agent/Separator.md +1 -1
  65. package/dist/agent/SequenceFigure.md +28 -0
  66. package/dist/agent/Sheet.md +1 -1
  67. package/dist/agent/Skeleton.md +1 -1
  68. package/dist/agent/Slider.md +1 -1
  69. package/dist/agent/Sparkline.md +1 -1
  70. package/dist/agent/Spinner.md +1 -1
  71. package/dist/agent/StatusDot.md +1 -1
  72. package/dist/agent/StatusPill.md +1 -1
  73. package/dist/agent/Steps.md +1 -1
  74. package/dist/agent/Switch.md +1 -1
  75. package/dist/agent/Table.md +1 -1
  76. package/dist/agent/Tabs.md +1 -1
  77. package/dist/agent/Tag.md +1 -1
  78. package/dist/agent/Textarea.md +1 -1
  79. package/dist/agent/Toast.md +1 -1
  80. package/dist/agent/ToggleGroup.md +1 -1
  81. package/dist/agent/Tooltip.md +1 -1
  82. package/dist/agent/TreemapChart.md +1 -1
  83. package/dist/agent/WaterfallChart.md +1 -1
  84. package/dist/agent/WorkflowFigure.md +28 -0
  85. package/dist/agent/catalog.json +223 -3
  86. package/dist/agent/index.md +15 -1
  87. package/dist/charts/index.d.ts +2 -1
  88. package/dist/charts/lib/export.d.ts +33 -43
  89. package/dist/charts/lib/export.js +16 -158
  90. package/dist/charts/lib/export.js.map +1 -1
  91. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +49 -0
  92. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +135 -0
  93. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -0
  94. package/dist/diagrams/DataflowFigure/DataflowFigure.d.ts +28 -0
  95. package/dist/diagrams/DataflowFigure/DataflowFigure.js +129 -0
  96. package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -0
  97. package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +64 -0
  98. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +222 -0
  99. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -0
  100. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +72 -0
  101. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +125 -0
  102. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -0
  103. package/dist/diagrams/DiagramInspector/DiagramInspector.d.ts +70 -0
  104. package/dist/diagrams/DiagramInspector/DiagramInspector.js +94 -0
  105. package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -0
  106. package/dist/diagrams/DiagramLegend/DiagramLegend.d.ts +42 -0
  107. package/dist/diagrams/DiagramLegend/DiagramLegend.js +35 -0
  108. package/dist/diagrams/DiagramLegend/DiagramLegend.js.map +1 -0
  109. package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +61 -0
  110. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +79 -0
  111. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -0
  112. package/dist/diagrams/DiagramToolbar/DiagramToolbar.d.ts +48 -0
  113. package/dist/diagrams/DiagramToolbar/DiagramToolbar.js +53 -0
  114. package/dist/diagrams/DiagramToolbar/DiagramToolbar.js.map +1 -0
  115. package/dist/diagrams/LifecycleFigure/LifecycleFigure.d.ts +31 -0
  116. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +202 -0
  117. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -0
  118. package/dist/diagrams/SequenceFigure/SequenceFigure.d.ts +38 -0
  119. package/dist/diagrams/SequenceFigure/SequenceFigure.js +231 -0
  120. package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -0
  121. package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +34 -0
  122. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +211 -0
  123. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -0
  124. package/dist/diagrams/index.d.ts +16 -0
  125. package/dist/diagrams/index.js +19 -0
  126. package/dist/diagrams/index.js.map +1 -0
  127. package/dist/diagrams/lib/frame.d.ts +97 -0
  128. package/dist/diagrams/lib/frame.js +116 -0
  129. package/dist/diagrams/lib/frame.js.map +1 -0
  130. package/dist/diagrams/lib/geometry.d.ts +114 -0
  131. package/dist/diagrams/lib/geometry.js +113 -0
  132. package/dist/diagrams/lib/geometry.js.map +1 -0
  133. package/dist/diagrams/lib/legend.d.ts +32 -0
  134. package/dist/diagrams/lib/legend.js +84 -0
  135. package/dist/diagrams/lib/legend.js.map +1 -0
  136. package/dist/diagrams/lib/marks.d.ts +207 -0
  137. package/dist/diagrams/lib/marks.js +480 -0
  138. package/dist/diagrams/lib/marks.js.map +1 -0
  139. package/dist/diagrams/lib/route.d.ts +85 -0
  140. package/dist/diagrams/lib/route.js +178 -0
  141. package/dist/diagrams/lib/route.js.map +1 -0
  142. package/dist/diagrams/lib/wires.d.ts +59 -0
  143. package/dist/diagrams/lib/wires.js +90 -0
  144. package/dist/diagrams/lib/wires.js.map +1 -0
  145. package/dist/diagrams/spec.d.ts +353 -0
  146. package/dist/diagrams/spec.js +1 -0
  147. package/dist/diagrams/spec.js.map +1 -0
  148. package/dist/lib/svg-export.d.ts +138 -0
  149. package/dist/lib/svg-export.js +198 -0
  150. package/dist/lib/svg-export.js.map +1 -0
  151. package/dist/semantic.css +34 -0
  152. package/dist/styles.css +1 -1
  153. package/dist/tokens.d.ts +16 -0
  154. package/dist/tokens.js +82 -0
  155. package/dist/tokens.json +82 -0
  156. package/package.json +5 -1
  157. package/skills/misoto22-design/SKILL.md +21 -7
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/lib/marks.tsx"],"sourcesContent":["/**\n * The marks every figure is drawn out of: a plate, a sigil, a line, a band.\n *\n * THE PROBLEM THIS FILE SOLVES. Archify separates seven kinds of node by hue —\n * a cyan frontend, an emerald service, a rose security boundary. This system\n * has no hue to spend: the only chroma left in it is bound to state, and the\n * rest is paper and ink. So the kind is carried TWICE, by a drawn sigil and by\n * a word, on one eyebrow line above the name.\n *\n * Two carriers rather than one, and the redundancy is the design. A 12-unit\n * glyph alone is a guess — a cylinder and a queue are four grey pixels apart at\n * reading size, and a reader who has not learnt the key is looking at smudges\n * that cost 30 units of the plate's width. A word alone scans slowly: seven\n * boxes each reading SERVICE in the same weight is a column of text, not a\n * diagram. Together the sigil does the scanning and the word does the\n * disambiguating, and the key that teaches the sigil shows the pair.\n *\n * THE HIERARCHY IS WEIGHT, RULE AND REVERSAL — the system's own law 7, and the\n * only one available once colour is spent. A plate is paper on a hairline; the\n * one node a figure is ABOUT is ink-filled; a boundary is a rule and never a\n * ground; a main path is a heavier line rather than a different one.\n *\n * Everything paints through CSS rather than through a presentation attribute,\n * because `fill=\"var(--ink)\"` is not valid markup — a custom property has to\n * reach an SVG through a style rule. That indirection is also what makes the\n * export work: `getComputedStyle` has already substituted the variable, so a\n * serialised clone carries a real colour rather than a name nothing resolves.\n */\n\nimport type { ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport type { LifecycleStateKind, NodeKind, Variant } from '../spec'\nimport { round, TYPE, textWidth, wrapText, type Box } from './geometry'\n\n/** The corner radius on a plate, tracking the system's ladder. */\nconst PLATE_RADIUS = '[rx:var(--radius-sm)] [ry:var(--radius-sm)]' as const\n\n/** What each kind is called on a plate's eyebrow. Short, because it is a kicker. */\nexport const KIND_WORD: Record<NodeKind, string> = {\n frontend: 'CLIENT',\n backend: 'SERVICE',\n database: 'DATA',\n cloud: 'CLOUD',\n security: 'POLICY',\n messagebus: 'QUEUE',\n external: 'EXTERNAL',\n}\n\n/**\n * The marker definitions every figure needs.\n *\n * Rendered once per figure. The ids are namespaced by the figure's own id\n * because two diagrams on one page would otherwise share one `<defs>`, and the\n * second figure's arrowheads would silently take the first figure's paint —\n * SVG ids are document-global, and a duplicate is resolved to whichever came\n * first rather than to the nearest one.\n */\nexport function DiagramDefs({ uid }: { uid: string }) {\n return (\n <defs>\n <marker\n id={`${uid}-arrow`}\n markerWidth=\"11\"\n markerHeight=\"11\"\n refX=\"9.5\"\n refY=\"5.5\"\n orient=\"auto-start-reverse\"\n markerUnits=\"userSpaceOnUse\"\n >\n <path d=\"M 0.6 1.4 L 10 5.5 L 0.6 9.6 Z\" className=\"fill-(--diagram-line)\" />\n </marker>\n <marker\n id={`${uid}-arrow-strong`}\n markerWidth=\"12\"\n markerHeight=\"12\"\n refX=\"10.5\"\n refY=\"6\"\n orient=\"auto-start-reverse\"\n markerUnits=\"userSpaceOnUse\"\n >\n <path d=\"M 0.6 1.6 L 11 6 L 0.6 10.4 Z\" className=\"fill-(--diagram-line-strong)\" />\n </marker>\n {/* An open head, for a reply. A reader distinguishes a return from a call\n faster by the head than by the dash pattern, which is why the two are\n different shapes rather than the same shape in two weights. */}\n <marker\n id={`${uid}-arrow-open`}\n markerWidth=\"11\"\n markerHeight=\"11\"\n refX=\"9.5\"\n refY=\"5.5\"\n orient=\"auto-start-reverse\"\n markerUnits=\"userSpaceOnUse\"\n >\n <path\n d=\"M 1 1.6 L 9.6 5.5 L 1 9.4\"\n fill=\"none\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"stroke-(--diagram-line-soft)\"\n />\n </marker>\n </defs>\n )\n}\n\n/**\n * The 14×14 mark that says what kind of thing a node is.\n *\n * Drawn on a 14-unit grid at a 1.35 stroke. Both numbers went up from the first\n * pass and both had to: at 12 units and a 1.2 stroke the seven forms were a few\n * grey pixels apart at reading size, which is not a distinction — it is a\n * smudge. Every path is closed geometry rather than an icon font, because a\n * figure has to serialise into a standalone file and a font the exported\n * document cannot fetch renders as a box.\n */\nexport function Sigil({\n kind,\n x,\n y,\n reversed,\n}: {\n kind: NodeKind\n x: number\n y: number\n reversed?: boolean\n}) {\n const stroke = cn(\n 'fill-none [stroke-width:1.35] [stroke-linejoin:round] [stroke-linecap:round]',\n reversed ? 'stroke-(--diagram-plate-ink)' : 'stroke-(--diagram-sigil)',\n )\n\n return (\n <g transform={`translate(${round(x)}, ${round(y)})`} aria-hidden=\"true\">\n {kind === 'frontend' && (\n <g className={stroke}>\n <rect x=\"0.8\" y=\"2\" width=\"12.4\" height=\"10\" rx=\"1.6\" />\n <path d=\"M 0.8 5.2 H 13.2\" />\n <path d=\"M 3 3.6 H 4\" />\n </g>\n )}\n {kind === 'backend' && (\n <g className={stroke}>\n <rect x=\"0.8\" y=\"1.8\" width=\"12.4\" height=\"4.4\" rx=\"1.2\" />\n <rect x=\"0.8\" y=\"7.8\" width=\"12.4\" height=\"4.4\" rx=\"1.2\" />\n <path d=\"M 3 4 H 4.2\" />\n <path d=\"M 3 10 H 4.2\" />\n </g>\n )}\n {kind === 'database' && (\n <g className={stroke}>\n <ellipse cx=\"7\" cy=\"3.4\" rx=\"6\" ry=\"2.4\" />\n <path d=\"M 1 3.4 V 10.6 A 6 2.4 0 0 0 13 10.6 V 3.4\" />\n <path d=\"M 1 7 A 6 2.4 0 0 0 13 7\" />\n </g>\n )}\n {kind === 'cloud' && (\n <g className={stroke}>\n <path d=\"M 3.4 10.8 A 2.9 2.9 0 0 1 3.5 5 A 3.8 3.8 0 0 1 10.5 4.2 A 3.2 3.2 0 0 1 11 10.8 Z\" />\n </g>\n )}\n {kind === 'security' && (\n <g className={stroke}>\n <path d=\"M 7 1 L 12.5 3.1 V 7 C 12.5 10 10 12.1 7 13 C 4 12.1 1.5 10 1.5 7 V 3.1 Z\" />\n <path d=\"M 4.7 7 L 6.4 8.7 L 9.4 5.3\" />\n </g>\n )}\n {kind === 'messagebus' && (\n <g className={stroke}>\n <path d=\"M 0.9 2.8 H 13.1\" />\n <path d=\"M 0.9 11.2 H 13.1\" />\n <path d=\"M 3.6 5.2 V 8.8\" />\n <path d=\"M 7 5.2 V 8.8\" />\n <path d=\"M 10.4 5.2 V 8.8\" />\n </g>\n )}\n {kind === 'external' && (\n <g className={stroke}>\n <path d=\"M 7.4 1.8 H 1.6 V 12.2 H 12.4 V 6.4\" />\n <path d=\"M 8.4 5.4 L 13 1\" />\n <path d=\"M 9.6 0.9 H 13.1 V 4.4\" />\n </g>\n )}\n </g>\n )\n}\n\n/** How a plate is painted, given what the specification insisted on. */\nfunction plateClasses(variant: Variant | undefined, muted: boolean): string {\n switch (variant) {\n case 'emphasis':\n return 'fill-(--diagram-plate) stroke-(--diagram-plate) [stroke-width:1]'\n case 'security':\n return 'fill-(--diagram-node) stroke-(--diagram-rule-hard) [stroke-width:1.3] [stroke-dasharray:5_3]'\n case 'dashed':\n return 'fill-(--diagram-node) stroke-(--diagram-rule) [stroke-width:1] [stroke-dasharray:4_3]'\n default:\n return muted\n ? 'fill-(--diagram-node-2) stroke-(--diagram-rule) [stroke-width:1]'\n : 'fill-(--diagram-node) stroke-(--diagram-rule) [stroke-width:1]'\n }\n}\n\n/**\n * A plate's internal vertical rhythm, in user units from its top edge.\n *\n * Written out as one object rather than computed inline in three renderers,\n * because these numbers ARE the plate: change one and the card stops looking\n * like a card. The eyebrow sits high and tight, the name is the only thing set\n * at reading size, and the sublabel hangs under it in the meta voice.\n */\nexport const PLATE = {\n padX: 15,\n eyebrow: 19,\n label: 39,\n sub: 53,\n lineStep: 17,\n /** Where the eyebrow lands when there is none — the label moves up into it. */\n riseWithoutEyebrow: 16,\n} as const\n\n/**\n * How tall a plate has to be to hold what it was given.\n *\n * A declared height is a FLOOR, not a ceiling. When a specification asks for a\n * 60-unit box and then puts two lines of name and a qualifier in it, honouring\n * the 60 prints the qualifier through the bottom rule — which reads as a\n * rendering fault rather than as a box asked to hold too much. Growing is\n * visible and correct; clipping is invisible and wrong.\n */\nexport function plateHeight(\n labelLines: number,\n hasSub: boolean,\n hasEyebrow: boolean,\n floor = 0,\n): number {\n const last = hasSub ? PLATE.sub : PLATE.label\n const content =\n last + (labelLines - 1) * PLATE.lineStep + 16 - (hasEyebrow ? 0 : PLATE.riseWithoutEyebrow)\n return Math.max(floor, Math.ceil(content))\n}\n\nexport interface NodePlateProps {\n box: Box\n label: string\n sublabel?: string\n tag?: string\n kind?: NodeKind\n variant?: Variant\n /** Printed in the trailing corner in mono — a step number. */\n step?: string\n /** Paints the plate on the recessed ground rather than on paper. */\n muted?: boolean\n /** Lit by a guided chapter or a reader's focus. */\n active?: boolean\n /** Dimmed because something else is lit. */\n dimmed?: boolean\n onSelect?: () => void\n /** A stable hook the viewer chrome and the tests both address nodes by. */\n nodeId?: string\n}\n\n/**\n * One box, with everything that can be printed inside it.\n *\n * Three registers, top to bottom, and each is a different voice on purpose: a\n * mono kicker naming the KIND, the name at reading size in the interface face,\n * and the qualifier back in mono. That is the same ladder a record on a page\n * uses — the diagram is not inventing a second typography for itself.\n *\n * WHAT IT IS NOT is a button, and that is deliberate. The `<svg>` around it\n * carries `role=\"img\"`, which makes everything inside it presentational to\n * assistive technology — so a `role=\"button\"` here would announce a control a\n * screen reader can never reach. The keyboard route to the same selection is\n * the figure's own summary list, which is a real `<ul>` of real `<button>`s\n * sitting beside the picture. `onSelect` here is the POINTER path to that same\n * action, and nothing else.\n */\nexport function NodePlate({\n box,\n label,\n sublabel,\n tag,\n kind,\n variant,\n step,\n muted,\n active,\n dimmed,\n onSelect,\n nodeId,\n}: NodePlateProps) {\n const reversed = variant === 'emphasis'\n const lines = wrapText(label, TYPE.label, box.w - PLATE.padX * 2, 2)\n\n // The eyebrow slot carries the most specific thing known about this node: the\n // author's own tag when there is one, and the kind word otherwise. Giving the\n // tag a row of its own instead cost every tagged plate 22 units of height for\n // one short string, and left the plate a portrait rectangle in a figure of\n // landscape ones. The sigil never moves, so the kind is still on the plate\n // either way — which is what makes the substitution safe.\n const eyebrow = tag ?? (kind ? KIND_WORD[kind] : undefined)\n const hasEyebrow = Boolean(eyebrow || step)\n const rise = hasEyebrow ? 0 : PLATE.riseWithoutEyebrow\n const labelY = box.y + PLATE.label - rise\n const subY = box.y + PLATE.sub - rise + (lines.length - 1) * PLATE.lineStep\n\n return (\n <g\n data-node={nodeId}\n data-active={active ? '' : undefined}\n className={cn(\n 'transition-opacity duration-(--duration-fast)',\n dimmed && 'opacity-25',\n onSelect && 'cursor-pointer',\n )}\n onClick={onSelect}\n >\n <rect\n x={round(box.x)}\n y={round(box.y)}\n width={round(box.w)}\n height={round(box.h)}\n rx={6}\n ry={6}\n className={cn(PLATE_RADIUS, plateClasses(variant, Boolean(muted)))}\n />\n\n {active && (\n <rect\n x={round(box.x) - 3.5}\n y={round(box.y) - 3.5}\n width={round(box.w) + 7}\n height={round(box.h) + 7}\n rx={9}\n ry={9}\n fill=\"none\"\n className=\"stroke-(--accent) [stroke-width:1.5]\"\n />\n )}\n\n {kind && (\n <Sigil\n kind={kind}\n x={box.x + PLATE.padX}\n y={box.y + PLATE.eyebrow - 11}\n reversed={reversed}\n />\n )}\n\n {eyebrow && (\n <text\n x={round(box.x + PLATE.padX + (kind ? 20 : 0))}\n y={round(box.y + PLATE.eyebrow)}\n className={cn(\n 'font-mono [font-size:8.5px] [letter-spacing:0.13em] uppercase',\n reversed ? 'fill-(--diagram-plate-ink) opacity-70' : 'fill-(--diagram-ink-3)',\n )}\n >\n {eyebrow}\n </text>\n )}\n\n {step && (\n <text\n x={round(box.x + box.w - PLATE.padX)}\n y={round(box.y + PLATE.eyebrow)}\n textAnchor=\"end\"\n className={cn(\n 'font-mono [font-size:9px] [letter-spacing:0.1em]',\n reversed ? 'fill-(--diagram-plate-ink) opacity-65' : 'fill-(--diagram-ink-3)',\n )}\n >\n {step}\n </text>\n )}\n\n {lines.map((line, index) => (\n <text\n key={index}\n x={round(box.x + PLATE.padX)}\n y={round(labelY + index * PLATE.lineStep)}\n className={cn(\n 'font-sans [font-size:14px] [letter-spacing:-0.005em]',\n reversed ? 'fill-(--diagram-plate-ink)' : 'fill-(--diagram-ink)',\n )}\n >\n {line}\n </text>\n ))}\n\n {sublabel && (\n <text\n x={round(box.x + PLATE.padX)}\n y={round(subY)}\n className={cn(\n 'font-mono [font-size:10px]',\n reversed ? 'fill-(--diagram-plate-ink) opacity-65' : 'fill-(--diagram-ink-3)',\n )}\n >\n {sublabel}\n </text>\n )}\n\n </g>\n )\n}\n\n/**\n * A mono chip: a version, a region, a protocol, a data classification.\n *\n * A rule and a word, with no fill under it. The filled chip this replaced put a\n * third ground inside a plate that already sat on a figure that sat on the\n * page, and three grounds deep is where a diagram stops reading as structure\n * and starts reading as packaging.\n */\nexport function Chip({\n x,\n y,\n text,\n align = 'start',\n reversed,\n}: {\n x: number\n y: number\n text: string\n align?: 'start' | 'middle' | 'end'\n reversed?: boolean\n}) {\n const width = textWidth(text, TYPE.chip) + 12\n const left = align === 'end' ? x - width : align === 'middle' ? x - width / 2 : x\n\n return (\n <g aria-hidden=\"true\">\n <rect\n x={round(left)}\n y={round(y - 7.5)}\n width={round(width)}\n height={15}\n rx={3}\n ry={3}\n className={cn(\n '[stroke-width:0.9] fill-(--diagram-surface)',\n reversed ? 'stroke-(--diagram-plate-ink) opacity-40' : 'stroke-(--diagram-rule)',\n )}\n />\n <text\n x={round(left + width / 2)}\n y={round(y + 2.8)}\n textAnchor=\"middle\"\n className={cn(\n 'font-mono [font-size:9px] [letter-spacing:0.05em]',\n reversed ? 'fill-(--diagram-plate-ink) opacity-70' : 'fill-(--diagram-ink-3)',\n )}\n >\n {text}\n </text>\n </g>\n )\n}\n\n/**\n * How a line is painted: the colour as a class, the geometry as attributes.\n *\n * THE SPLIT IS NOT ARBITRARY, and getting it wrong cost an afternoon. Tailwind\n * generates a rule only for a class name it can find as a LITERAL in the\n * source. A width built at runtime — `` `[stroke-width:${weight}]` `` — is a\n * class that never gets a rule, so the browser falls back to SVG's default\n * 1px, silently, on exactly the lines a figure most needs to be heavier. The\n * main path came out the same weight as a side branch and nothing said so.\n *\n * Colour still goes through a class, because a paint has to resolve a custom\n * property and `stroke=\"var(--x)\"` is not valid as an attribute. Width and dash\n * pattern are plain numbers, so they go through presentation attributes where\n * no build step has to have predicted them — and `getComputedStyle` reports an\n * attribute exactly as it reports a rule, so the export still picks them up.\n */\nexport function edgeClasses(variant?: Variant | 'return'): string {\n switch (variant) {\n case 'emphasis':\n return 'stroke-(--diagram-line-strong)'\n case 'dashed':\n case 'return':\n return 'stroke-(--diagram-line-soft)'\n default:\n return 'stroke-(--diagram-line)'\n }\n}\n\n/** A line's width and dash pattern, as presentation attributes. */\nexport function edgeStroke(\n variant?: Variant | 'return',\n weight?: number,\n): { strokeWidth: number; strokeDasharray?: string } {\n switch (variant) {\n case 'emphasis':\n return { strokeWidth: weight ?? 2.1 }\n case 'security':\n return { strokeWidth: weight ?? 1.4, strokeDasharray: '7 4' }\n case 'dashed':\n case 'return':\n return { strokeWidth: weight ?? 1.3, strokeDasharray: '4.5 4' }\n default:\n return { strokeWidth: weight ?? 1.4 }\n }\n}\n\n/** Which arrowhead a line ends in. */\nexport function edgeMarker(variant?: Variant | 'return'): 'arrow' | 'arrow-strong' | 'arrow-open' {\n if (variant === 'emphasis') return 'arrow-strong'\n if (variant === 'return') return 'arrow-open'\n return 'arrow'\n}\n\nexport interface EdgeLabelProps {\n x: number\n y: number\n axis: 'x' | 'y'\n text: string\n /** A mono chip printed under the wording — a data classification. */\n chip?: string\n /** A second, quieter line — a condition, a timeout, a retry count. */\n note?: string\n}\n\n/**\n * A relationship's wording, on a mask that punches the line out from under it.\n *\n * The mask is not decoration. A label drawn straight onto a line is unreadable\n * at every size, and the two repairs a renderer can make are moving the label\n * or hiding the line behind it. Moving it changes which segment the wording\n * belongs to, which is a semantic change; masking is the one that leaves the\n * meaning alone.\n *\n * Set in mono rather than in the interface face. The wording on a line is an\n * ANNOTATION on the picture, not a second rank of content — and at the sans\n * face it competed with the names on the plates, which is the one comparison it\n * must lose.\n */\nexport function EdgeLabel({ x, y, axis, text, chip, note }: EdgeLabelProps) {\n const width = textWidth(text, TYPE.edge) + 10\n const noteWidth = note ? textWidth(note, TYPE.chip) + 10 : 0\n const cy = y + (axis === 'x' ? -10 : 0)\n\n return (\n <g aria-hidden=\"true\">\n <rect\n x={round(x - width / 2)}\n y={round(cy - 7)}\n width={round(width)}\n height={14}\n className=\"fill-(--diagram-surface)\"\n />\n <text\n x={round(x)}\n y={round(cy + 3.5)}\n textAnchor=\"middle\"\n className=\"fill-(--diagram-ink-2) font-mono [font-size:10px]\"\n >\n {text}\n </text>\n {note && (\n <>\n <rect\n x={round(x - noteWidth / 2)}\n y={round(cy + 6)}\n width={round(noteWidth)}\n height={13}\n className=\"fill-(--diagram-surface)\"\n />\n <text\n x={round(x)}\n y={round(cy + 15.5)}\n textAnchor=\"middle\"\n className=\"fill-(--diagram-ink-3) font-mono [font-size:9px]\"\n >\n {note}\n </text>\n </>\n )}\n {chip && <Chip x={x} y={cy + (note ? 33 : 20)} text={chip} align=\"middle\" />}\n </g>\n )\n}\n\n/**\n * A labelled frame around a set of boxes: a region, a security group, a phase,\n * a process group.\n *\n * A FRAME IS A RULE AND NOT A GROUND. Archify washes its regions with a tint,\n * which this system cannot copy for two reasons: a second ground inside a paper\n * figure reads as packaging rather than as structure — the same argument the\n * containment `Diagram` primitive makes about nested boxes — and every\n * relationship label crossing the wash would then need to know which of two\n * grounds its mask is punching through, which is a question a label cannot\n * answer. `muted` therefore washes only where a band IS the ground it labels: a\n * workflow's exception lane, which nothing routes across.\n *\n * The label sits ABOVE the top rule rather than inside the frame or on it.\n * Inside, it costs a line of vertical space in every frame whether or not the\n * frame had one to spare. ON the rule — which is where it started — it has to\n * punch a mask through the frame, and that mask then competes for the same few\n * units of clear ground as any relationship label routed along the frame's own\n * edge: two captions, two white plates, one on top of the other. Above the\n * rule it needs no mask, leaves the frame unbroken, and reads as a caption for\n * the frame rather than as its first child.\n */\nexport function BandFrame({\n box,\n label,\n dashed,\n muted,\n}: {\n box: Box\n label?: string\n dashed?: boolean\n muted?: boolean\n}) {\n return (\n <g aria-hidden=\"true\">\n <rect\n x={round(box.x)}\n y={round(box.y)}\n width={round(box.w)}\n height={round(box.h)}\n rx={10}\n ry={10}\n className={cn(\n '[rx:var(--radius-lg)] [ry:var(--radius-lg)]',\n muted ? 'fill-(--diagram-band)' : 'fill-none',\n dashed\n ? 'stroke-(--diagram-rule-hard) [stroke-width:1.2] [stroke-dasharray:6_4]'\n : 'stroke-(--diagram-rule) [stroke-width:1]',\n )}\n />\n {label && (\n <text\n x={round(box.x + 2)}\n y={round(box.y - 7)}\n className=\"fill-(--diagram-ink-3) font-mono [font-size:9px] [letter-spacing:0.14em] uppercase\"\n >\n {label}\n </text>\n )}\n </g>\n )\n}\n\n/**\n * The plate shape for a lifecycle state.\n *\n * This is the one place the system spends colour, and it spends exactly the two\n * tokens it reserves for state: `--success` on a terminal success, `--danger`\n * on a terminal failure. Every other kind is carried by shape — a filled cap\n * for a start, a diamond for a decision, a dashed frame for a wait, a cut\n * corner for something outside the system — so a greyscale print loses the two\n * outcomes' hue and keeps all six other distinctions.\n */\nexport function StatePlate({\n box,\n kind,\n children,\n}: {\n box: Box\n kind: LifecycleStateKind\n children?: ReactNode\n}) {\n const { x, y, w, h } = box\n const common = '[stroke-width:1.3]'\n\n if (kind === 'decision') {\n const cx = x + w / 2\n const cy = y + h / 2\n return (\n <g>\n <path\n d={`M ${round(cx)} ${round(y)} L ${round(x + w)} ${round(cy)} L ${round(cx)} ${round(y + h)} L ${round(x)} ${round(cy)} Z`}\n className={cn('fill-(--diagram-node) stroke-(--diagram-rule-hard)', common)}\n />\n {children}\n </g>\n )\n }\n\n if (kind === 'external') {\n const cut = 13\n return (\n <g>\n <path\n d={`M ${round(x)} ${round(y)} H ${round(x + w - cut)} L ${round(x + w)} ${round(y + cut)} V ${round(y + h)} H ${round(x)} Z`}\n className={cn('fill-(--diagram-node-2) stroke-(--diagram-rule)', common)}\n />\n {children}\n </g>\n )\n }\n\n const shape =\n kind === 'start'\n ? 'fill-(--diagram-plate) stroke-(--diagram-plate)'\n : kind === 'success'\n ? 'fill-(--success-wash) stroke-(--success)'\n : kind === 'failure'\n ? 'fill-(--danger-wash) stroke-(--danger)'\n : kind === 'waiting'\n ? 'fill-(--diagram-node) stroke-(--diagram-rule) [stroke-dasharray:5_3]'\n : kind === 'neutral'\n ? 'fill-(--diagram-node-2) stroke-(--diagram-rule)'\n : 'fill-(--diagram-node) stroke-(--diagram-rule-hard)'\n\n const radius = kind === 'start' ? h / 2 : 6\n\n return (\n <g>\n <rect\n x={round(x)}\n y={round(y)}\n width={round(w)}\n height={round(h)}\n rx={radius}\n ry={radius}\n className={cn(kind === 'start' ? '' : PLATE_RADIUS, shape, common)}\n />\n {children}\n </g>\n )\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,85 @@
1
+ import { Side, RouteMode, Point } from '../spec.js';
2
+ import { Box } from './geometry.js';
3
+
4
+ /**
5
+ * Getting a line from one box to another.
6
+ *
7
+ * The routing rule this module keeps, and the reason it is worth having at all:
8
+ * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face
9
+ * must leave it going right, and a line that says it arrives at a top face must
10
+ * arrive going down. A router that takes the shortest path between two anchor
11
+ * points breaks that contract constantly — it produces a segment that grazes
12
+ * the box it just left, and a reader cannot tell which of two adjacent boxes an
13
+ * arrow belongs to. Every path below therefore begins and ends with a stub
14
+ * perpendicular to its face, and the interesting geometry happens between the
15
+ * two stubs.
16
+ *
17
+ * The second rule is about crowding. When several lines share one face they are
18
+ * spread across it rather than stacked on its midpoint, because two arrowheads
19
+ * landing on the same coordinate is one arrowhead as far as a reader is
20
+ * concerned. Explicit routing controls opt out of the spread: an author who
21
+ * pinned a channel has already decided where the line goes.
22
+ */
23
+
24
+ interface RouteRequest {
25
+ from: Box;
26
+ to: Box;
27
+ fromSide?: Side;
28
+ toSide?: Side;
29
+ mode?: RouteMode | string;
30
+ via?: Point[];
31
+ channelX?: number;
32
+ channelY?: number;
33
+ /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */
34
+ fromOffset?: number;
35
+ toOffset?: number;
36
+ }
37
+ interface RoutedEdge {
38
+ /** The `d` attribute, corners already rounded. */
39
+ d: string;
40
+ /** The polyline before rounding — what the label placer reads. */
41
+ points: [number, number][];
42
+ /** Where a label wants to sit, and how the segment under it runs. */
43
+ label: {
44
+ x: number;
45
+ y: number;
46
+ axis: 'x' | 'y';
47
+ };
48
+ /** The last segment's direction, for the arrowhead. */
49
+ angle: number;
50
+ }
51
+ /**
52
+ * The polyline as a path, with the corners turned rather than mitred.
53
+ *
54
+ * A quadratic through the corner point, pulled back along both legs by the
55
+ * radius. The pull-back is clamped to a third of each leg so a short segment
56
+ * between two turns cannot produce a curve that overshoots its own endpoint —
57
+ * which renders as a hook and is the classic artefact of rounding a polyline
58
+ * without checking the leg lengths.
59
+ */
60
+ declare function roundedPath(points: [number, number][], radius?: number): string;
61
+ /** Routes one relationship. */
62
+ declare function routeEdge(request: RouteRequest): RoutedEdge;
63
+ /** One line's use of one face: which face, and which end of which line it is. */
64
+ interface PortUse {
65
+ /** The face, as `nodeId:side`. Two lines sharing this string share a face. */
66
+ face: string;
67
+ /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */
68
+ ref: string;
69
+ }
70
+ /**
71
+ * Which share of its face each line gets, when several use the same one.
72
+ *
73
+ * A single line keeps the midpoint — spreading one line only moves it off
74
+ * centre for no reason. Lines that carry an explicit routing control are
75
+ * excluded by the caller before they get here, so a pinned channel is never
76
+ * nudged out from under its pin.
77
+ *
78
+ * Endpoints are keyed separately rather than by line, because a line's two ends
79
+ * land on two different faces with two different amounts of company. Keying by
80
+ * line gave both ends the same offset, which on a figure with one crowded face
81
+ * and one empty one visibly skewed every arrow that touched the empty side.
82
+ */
83
+ declare function spreadPorts(uses: PortUse[]): Map<string, number>;
84
+
85
+ export { type PortUse, type RouteRequest, type RoutedEdge, roundedPath, routeEdge, spreadPorts };
@@ -0,0 +1,178 @@
1
+ import { round } from "./geometry.js";
2
+ const STUB = 18;
3
+ const CORNER = 8;
4
+ function autoSides(from, to) {
5
+ const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w));
6
+ const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h));
7
+ if (gapX >= gapY) {
8
+ return to.x >= from.x ? ["right", "left"] : ["left", "right"];
9
+ }
10
+ return to.y >= from.y ? ["bottom", "top"] : ["top", "bottom"];
11
+ }
12
+ function port(box, side, offset = 0.5) {
13
+ const t = Math.min(0.88, Math.max(0.12, offset));
14
+ switch (side) {
15
+ case "left":
16
+ return [box.x, box.y + box.h * t];
17
+ case "right":
18
+ return [box.x + box.w, box.y + box.h * t];
19
+ case "top":
20
+ return [box.x + box.w * t, box.y];
21
+ case "bottom":
22
+ default:
23
+ return [box.x + box.w * t, box.y + box.h];
24
+ }
25
+ }
26
+ function stubFrom(anchor, side, length = STUB) {
27
+ switch (side) {
28
+ case "left":
29
+ return [anchor[0] - length, anchor[1]];
30
+ case "right":
31
+ return [anchor[0] + length, anchor[1]];
32
+ case "top":
33
+ return [anchor[0], anchor[1] - length];
34
+ case "bottom":
35
+ default:
36
+ return [anchor[0], anchor[1] + length];
37
+ }
38
+ }
39
+ const isHorizontal = (side) => side === "left" || side === "right";
40
+ function connect(a, b, fromSide, toSide, from, to, channelX, channelY) {
41
+ const fromH = isHorizontal(fromSide);
42
+ const toH = isHorizontal(toSide);
43
+ if (fromH && toH) {
44
+ const forward = fromSide === "right" && b[0] >= a[0] || fromSide === "left" && b[0] <= a[0];
45
+ const x = channelX ?? (forward ? (a[0] + b[0]) / 2 : (
46
+ // Behind: run out past the far edge of both boxes and come back.
47
+ fromSide === "right" ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB) : Math.min(a[0], to.x - STUB, from.x - STUB)
48
+ ));
49
+ if (Math.abs(a[1] - b[1]) < 0.5) return [a, b];
50
+ return [a, [x, a[1]], [x, b[1]], b];
51
+ }
52
+ if (!fromH && !toH) {
53
+ const forward = fromSide === "bottom" && b[1] >= a[1] || fromSide === "top" && b[1] <= a[1];
54
+ const y = channelY ?? (forward ? (a[1] + b[1]) / 2 : fromSide === "bottom" ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB) : Math.min(a[1], to.y - STUB, from.y - STUB));
55
+ if (Math.abs(a[0] - b[0]) < 0.5) return [a, b];
56
+ return [a, [a[0], y], [b[0], y], b];
57
+ }
58
+ return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b];
59
+ }
60
+ function simplify(points) {
61
+ const out = [];
62
+ for (const point of points) {
63
+ const last = out[out.length - 1];
64
+ if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue;
65
+ out.push(point);
66
+ }
67
+ for (let index = 1; index < out.length - 1; ) {
68
+ const [ax, ay] = out[index - 1];
69
+ const [bx, by] = out[index];
70
+ const [cx, cy] = out[index + 1];
71
+ const collinear = Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5 || Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5;
72
+ if (collinear) out.splice(index, 1);
73
+ else index += 1;
74
+ }
75
+ return out;
76
+ }
77
+ function roundedPath(points, radius = CORNER) {
78
+ if (points.length === 0) return "";
79
+ if (points.length === 1) return `M ${round(points[0][0])} ${round(points[0][1])}`;
80
+ let d = `M ${round(points[0][0])} ${round(points[0][1])}`;
81
+ for (let index = 1; index < points.length - 1; index += 1) {
82
+ const previous = points[index - 1];
83
+ const corner = points[index];
84
+ const next = points[index + 1];
85
+ const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1]);
86
+ const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1]);
87
+ const r = Math.min(radius, inLength / 3, outLength / 3);
88
+ if (r < 1) {
89
+ d += ` L ${round(corner[0])} ${round(corner[1])}`;
90
+ continue;
91
+ }
92
+ const enter = [
93
+ corner[0] + (previous[0] - corner[0]) / inLength * r,
94
+ corner[1] + (previous[1] - corner[1]) / inLength * r
95
+ ];
96
+ const leave = [
97
+ corner[0] + (next[0] - corner[0]) / outLength * r,
98
+ corner[1] + (next[1] - corner[1]) / outLength * r
99
+ ];
100
+ d += ` L ${round(enter[0])} ${round(enter[1])}`;
101
+ d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`;
102
+ }
103
+ const end = points[points.length - 1];
104
+ return `${d} L ${round(end[0])} ${round(end[1])}`;
105
+ }
106
+ function labelSpot(points) {
107
+ let best = { x: points[0][0], y: points[0][1], axis: "x", length: -1 };
108
+ for (let index = 0; index < points.length - 1; index += 1) {
109
+ const a = points[index];
110
+ const b = points[index + 1];
111
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
112
+ if (length > best.length) {
113
+ best = {
114
+ x: (a[0] + b[0]) / 2,
115
+ y: (a[1] + b[1]) / 2,
116
+ axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? "x" : "y",
117
+ length
118
+ };
119
+ }
120
+ }
121
+ return { x: best.x, y: best.y, axis: best.axis };
122
+ }
123
+ function routeEdge(request) {
124
+ const { from, to, via, channelX, channelY, mode = "auto" } = request;
125
+ const [autoFrom, autoTo] = autoSides(from, to);
126
+ const fromSide = request.fromSide ?? autoFrom;
127
+ const toSide = request.toSide ?? autoTo;
128
+ const start = port(from, fromSide, request.fromOffset);
129
+ const end = port(to, toSide, request.toOffset);
130
+ let points;
131
+ if (mode === "straight") {
132
+ points = [start, end];
133
+ } else {
134
+ const outward = stubFrom(start, fromSide);
135
+ const inward = stubFrom(end, toSide);
136
+ if (via && via.length > 0) {
137
+ points = [start, outward];
138
+ let cursor = outward;
139
+ for (const waypoint of via) {
140
+ const next = [waypoint[0], waypoint[1]];
141
+ points.push([next[0], cursor[1]], next);
142
+ cursor = next;
143
+ }
144
+ points.push([inward[0], cursor[1]], inward, end);
145
+ } else {
146
+ const forced = mode === "orthogonal-h" ? "h" : mode === "orthogonal-v" ? "v" : null;
147
+ const middle = forced === "h" ? [outward, [inward[0], outward[1]], inward] : forced === "v" ? [outward, [outward[0], inward[1]], inward] : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY);
148
+ points = [start, ...middle, end];
149
+ }
150
+ }
151
+ points = simplify(points);
152
+ const last = points[points.length - 1];
153
+ const previous = points[points.length - 2] ?? last;
154
+ const angle = Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180 / Math.PI;
155
+ return { d: roundedPath(points), points, label: labelSpot(points), angle };
156
+ }
157
+ function spreadPorts(uses) {
158
+ const byFace = /* @__PURE__ */ new Map();
159
+ for (const use of uses) {
160
+ const list = byFace.get(use.face);
161
+ if (list) list.push(use.ref);
162
+ else byFace.set(use.face, [use.ref]);
163
+ }
164
+ const offsets = /* @__PURE__ */ new Map();
165
+ for (const list of byFace.values()) {
166
+ if (list.length < 2) continue;
167
+ list.forEach((ref, position) => {
168
+ offsets.set(ref, (position + 1) / (list.length + 1));
169
+ });
170
+ }
171
+ return offsets;
172
+ }
173
+ export {
174
+ roundedPath,
175
+ routeEdge,
176
+ spreadPorts
177
+ };
178
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/lib/route.ts"],"sourcesContent":["/**\n * Getting a line from one box to another.\n *\n * The routing rule this module keeps, and the reason it is worth having at all:\n * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face\n * must leave it going right, and a line that says it arrives at a top face must\n * arrive going down. A router that takes the shortest path between two anchor\n * points breaks that contract constantly — it produces a segment that grazes\n * the box it just left, and a reader cannot tell which of two adjacent boxes an\n * arrow belongs to. Every path below therefore begins and ends with a stub\n * perpendicular to its face, and the interesting geometry happens between the\n * two stubs.\n *\n * The second rule is about crowding. When several lines share one face they are\n * spread across it rather than stacked on its midpoint, because two arrowheads\n * landing on the same coordinate is one arrowhead as far as a reader is\n * concerned. Explicit routing controls opt out of the spread: an author who\n * pinned a channel has already decided where the line goes.\n */\n\nimport type { Point, RouteMode, Side } from '../spec'\nimport { round, type Box } from './geometry'\n\n/** How far a line runs straight out of a face before it is allowed to turn. */\nconst STUB = 18\n\n/** The turn radius on an orthogonal corner. */\nconst CORNER = 8\n\nexport interface RouteRequest {\n from: Box\n to: Box\n fromSide?: Side\n toSide?: Side\n mode?: RouteMode | string\n via?: Point[]\n channelX?: number\n channelY?: number\n /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */\n fromOffset?: number\n toOffset?: number\n}\n\nexport interface RoutedEdge {\n /** The `d` attribute, corners already rounded. */\n d: string\n /** The polyline before rounding — what the label placer reads. */\n points: [number, number][]\n /** Where a label wants to sit, and how the segment under it runs. */\n label: { x: number; y: number; axis: 'x' | 'y' }\n /** The last segment's direction, for the arrowhead. */\n angle: number\n}\n\n/**\n * The face a line should use when the author did not say.\n *\n * Whichever axis separates the two boxes by more wins, and the choice is made\n * on the GAP rather than on centre distance. Two boxes side by side with a\n * small vertical offset separate horizontally, which is what a reader expects;\n * comparing centres would flip that pair to top/bottom as soon as the offset\n * exceeded half a box.\n */\nfunction autoSides(from: Box, to: Box): [Side, Side] {\n const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w))\n const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h))\n\n if (gapX >= gapY) {\n return to.x >= from.x ? ['right', 'left'] : ['left', 'right']\n }\n return to.y >= from.y ? ['bottom', 'top'] : ['top', 'bottom']\n}\n\n/** The point on a face where a line attaches, given its share of that face. */\nfunction port(box: Box, side: Side, offset = 0.5): [number, number] {\n const t = Math.min(0.88, Math.max(0.12, offset))\n switch (side) {\n case 'left':\n return [box.x, box.y + box.h * t]\n case 'right':\n return [box.x + box.w, box.y + box.h * t]\n case 'top':\n return [box.x + box.w * t, box.y]\n case 'bottom':\n default:\n return [box.x + box.w * t, box.y + box.h]\n }\n}\n\n/** One step out of a face, perpendicular to it. */\nfunction stubFrom(anchor: [number, number], side: Side, length = STUB): [number, number] {\n switch (side) {\n case 'left':\n return [anchor[0] - length, anchor[1]]\n case 'right':\n return [anchor[0] + length, anchor[1]]\n case 'top':\n return [anchor[0], anchor[1] - length]\n case 'bottom':\n default:\n return [anchor[0], anchor[1] + length]\n }\n}\n\nconst isHorizontal = (side: Side) => side === 'left' || side === 'right'\n\n/**\n * The polyline between two stub ends, kept orthogonal.\n *\n * Three cases, and they are the only three an axis-aligned router has:\n *\n * - **Facing across the same axis** (right→left, bottom→top). One dogleg\n * through a channel halfway between them, which is the shape everybody draws\n * by hand.\n * - **Turning a corner** (right→top). A single elbow, and WHICH elbow is\n * decided by the outgoing axis: leaving a vertical face means running\n * vertically first, or the line re-enters the box it just left.\n * - **Same-side or back-tracking** (right→right, or right→left with the target\n * behind). The channel goes OUTSIDE both boxes rather than between them,\n * because there is no room between them to go through.\n */\nfunction connect(\n a: [number, number],\n b: [number, number],\n fromSide: Side,\n toSide: Side,\n from: Box,\n to: Box,\n channelX?: number,\n channelY?: number,\n): [number, number][] {\n const fromH = isHorizontal(fromSide)\n const toH = isHorizontal(toSide)\n\n if (fromH && toH) {\n const forward =\n (fromSide === 'right' && b[0] >= a[0]) || (fromSide === 'left' && b[0] <= a[0])\n const x =\n channelX ??\n (forward\n ? (a[0] + b[0]) / 2\n : // Behind: run out past the far edge of both boxes and come back.\n fromSide === 'right'\n ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB)\n : Math.min(a[0], to.x - STUB, from.x - STUB))\n if (Math.abs(a[1] - b[1]) < 0.5) return [a, b]\n return [a, [x, a[1]], [x, b[1]], b]\n }\n\n if (!fromH && !toH) {\n const forward =\n (fromSide === 'bottom' && b[1] >= a[1]) || (fromSide === 'top' && b[1] <= a[1])\n const y =\n channelY ??\n (forward\n ? (a[1] + b[1]) / 2\n : fromSide === 'bottom'\n ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB)\n : Math.min(a[1], to.y - STUB, from.y - STUB))\n if (Math.abs(a[0] - b[0]) < 0.5) return [a, b]\n return [a, [a[0], y], [b[0], y], b]\n }\n\n // One elbow. The axis of the OUTGOING face decides which corner it is.\n return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b]\n}\n\n/** Drops points that repeat, and collapses three points on one straight run into two. */\nfunction simplify(points: [number, number][]): [number, number][] {\n const out: [number, number][] = []\n for (const point of points) {\n const last = out[out.length - 1]\n if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue\n out.push(point)\n }\n for (let index = 1; index < out.length - 1; ) {\n const [ax, ay] = out[index - 1]!\n const [bx, by] = out[index]!\n const [cx, cy] = out[index + 1]!\n const collinear =\n (Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5) ||\n (Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5)\n if (collinear) out.splice(index, 1)\n else index += 1\n }\n return out\n}\n\n/**\n * The polyline as a path, with the corners turned rather than mitred.\n *\n * A quadratic through the corner point, pulled back along both legs by the\n * radius. The pull-back is clamped to a third of each leg so a short segment\n * between two turns cannot produce a curve that overshoots its own endpoint —\n * which renders as a hook and is the classic artefact of rounding a polyline\n * without checking the leg lengths.\n */\nexport function roundedPath(points: [number, number][], radius = CORNER): string {\n if (points.length === 0) return ''\n if (points.length === 1) return `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n let d = `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n for (let index = 1; index < points.length - 1; index += 1) {\n const previous = points[index - 1]!\n const corner = points[index]!\n const next = points[index + 1]!\n\n const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1])\n const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1])\n const r = Math.min(radius, inLength / 3, outLength / 3)\n\n if (r < 1) {\n d += ` L ${round(corner[0])} ${round(corner[1])}`\n continue\n }\n\n const enter: [number, number] = [\n corner[0] + ((previous[0] - corner[0]) / inLength) * r,\n corner[1] + ((previous[1] - corner[1]) / inLength) * r,\n ]\n const leave: [number, number] = [\n corner[0] + ((next[0] - corner[0]) / outLength) * r,\n corner[1] + ((next[1] - corner[1]) / outLength) * r,\n ]\n\n d += ` L ${round(enter[0])} ${round(enter[1])}`\n d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`\n }\n\n const end = points[points.length - 1]!\n return `${d} L ${round(end[0])} ${round(end[1])}`\n}\n\n/**\n * Where the line's wording goes: the middle of its longest segment.\n *\n * The longest segment is chosen rather than the path's own midpoint because the\n * midpoint of a dogleg often lands on a corner, and a label on a corner sits\n * across both legs and masks the turn — which is the one part of the line that\n * carries information about where it went.\n */\nfunction labelSpot(points: [number, number][]): { x: number; y: number; axis: 'x' | 'y' } {\n let best = { x: points[0]![0], y: points[0]![1], axis: 'x' as 'x' | 'y', length: -1 }\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index]!\n const b = points[index + 1]!\n const length = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (length > best.length) {\n best = {\n x: (a[0] + b[0]) / 2,\n y: (a[1] + b[1]) / 2,\n axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? 'x' : 'y',\n length,\n }\n }\n }\n\n return { x: best.x, y: best.y, axis: best.axis }\n}\n\n/** Routes one relationship. */\nexport function routeEdge(request: RouteRequest): RoutedEdge {\n const { from, to, via, channelX, channelY, mode = 'auto' } = request\n\n const [autoFrom, autoTo] = autoSides(from, to)\n const fromSide = request.fromSide ?? autoFrom\n const toSide = request.toSide ?? autoTo\n\n const start = port(from, fromSide, request.fromOffset)\n const end = port(to, toSide, request.toOffset)\n\n let points: [number, number][]\n\n if (mode === 'straight') {\n // Straight means straight: centre to centre, clipped to both faces. No\n // stubs, because a stub would bend the one route whose whole promise is\n // that it does not bend.\n points = [start, end]\n } else {\n const outward = stubFrom(start, fromSide)\n const inward = stubFrom(end, toSide)\n\n if (via && via.length > 0) {\n // Waypoints are honoured literally and joined orthogonally. An author who\n // reached for `via` is overruling the router, and a router that then\n // \"improved\" the result would make the control useless.\n points = [start, outward]\n let cursor: [number, number] = outward\n for (const waypoint of via) {\n const next: [number, number] = [waypoint[0], waypoint[1]]\n points.push([next[0], cursor[1]], next)\n cursor = next\n }\n points.push([inward[0], cursor[1]], inward, end)\n } else {\n const forced =\n mode === 'orthogonal-h' ? ('h' as const) : mode === 'orthogonal-v' ? ('v' as const) : null\n\n const middle =\n forced === 'h'\n ? ([outward, [inward[0], outward[1]], inward] as [number, number][])\n : forced === 'v'\n ? ([outward, [outward[0], inward[1]], inward] as [number, number][])\n : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY)\n\n points = [start, ...middle, end]\n }\n }\n\n points = simplify(points)\n\n const last = points[points.length - 1]!\n const previous = points[points.length - 2] ?? last\n const angle = (Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180) / Math.PI\n\n return { d: roundedPath(points), points, label: labelSpot(points), angle }\n}\n\n/** One line's use of one face: which face, and which end of which line it is. */\nexport interface PortUse {\n /** The face, as `nodeId:side`. Two lines sharing this string share a face. */\n face: string\n /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */\n ref: string\n}\n\n/**\n * Which share of its face each line gets, when several use the same one.\n *\n * A single line keeps the midpoint — spreading one line only moves it off\n * centre for no reason. Lines that carry an explicit routing control are\n * excluded by the caller before they get here, so a pinned channel is never\n * nudged out from under its pin.\n *\n * Endpoints are keyed separately rather than by line, because a line's two ends\n * land on two different faces with two different amounts of company. Keying by\n * line gave both ends the same offset, which on a figure with one crowded face\n * and one empty one visibly skewed every arrow that touched the empty side.\n */\nexport function spreadPorts(uses: PortUse[]): Map<string, number> {\n const byFace = new Map<string, string[]>()\n for (const use of uses) {\n const list = byFace.get(use.face)\n if (list) list.push(use.ref)\n else byFace.set(use.face, [use.ref])\n }\n\n const offsets = new Map<string, number>()\n for (const list of byFace.values()) {\n if (list.length < 2) continue\n list.forEach((ref, position) => {\n offsets.set(ref, (position + 1) / (list.length + 1))\n })\n }\n return offsets\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,59 @@
1
+ import { ReactNode } from 'react';
2
+ import { EdgeBase, Variant } from '../spec.js';
3
+ import { Box } from './geometry.js';
4
+
5
+ /** One line to draw, once its two boxes are known. */
6
+ interface Wire {
7
+ edge: EdgeBase & {
8
+ variant?: Variant | 'return';
9
+ classification?: string;
10
+ note?: string;
11
+ };
12
+ from: Box;
13
+ to: Box;
14
+ /** Overrides the variant's own stroke weight — a main path drawn heavier. */
15
+ weight?: number;
16
+ /** An open arrowhead rather than a filled one. */
17
+ open?: boolean;
18
+ /**
19
+ * Whether this line's `via` / `channelX` / `channelY` mean anything here.
20
+ *
21
+ * A waypoint is an ABSOLUTE coordinate in whatever space the author's nodes
22
+ * were placed in. When a specification pins its nodes with `pos`, that space
23
+ * is this renderer's space too and the waypoints land exactly where they were
24
+ * meant to. When the nodes are placed from a lane and a column, this renderer
25
+ * chose the coordinates — so the author's waypoints refer to a grid that does
26
+ * not exist here, and following them drags lines across unrelated nodes.
27
+ *
28
+ * So the rule is per line and not per figure: honour the waypoints when both
29
+ * of a line's endpoints were placed absolutely, and route automatically
30
+ * otherwise. Defaults to false, which is the safe half.
31
+ */
32
+ keepWaypoints?: boolean;
33
+ }
34
+ /**
35
+ * Every line in a figure, routed together.
36
+ *
37
+ * Together rather than one at a time, because port spreading is a property of a
38
+ * FACE and not of a line: how far off centre a given arrow sits depends on how
39
+ * many others land on the same face, which no single line can know. Routing
40
+ * them independently is what produces three arrowheads stacked on one point.
41
+ *
42
+ * Lines that pin their own geometry — `via`, a channel, an explicit side — opt
43
+ * out of the spread. An author who placed a line has already answered the
44
+ * question the spread exists to answer, and moving it afterwards makes the
45
+ * control useless.
46
+ *
47
+ * Returns the paths and the labels separately because ORDER IS THE MASK: a
48
+ * label's plate has to be painted over every line in the figure, not merely
49
+ * over its own. Interleaving them puts the third line on top of the first
50
+ * line's label.
51
+ */
52
+ declare function renderWires(wires: Wire[], uid: string): {
53
+ paths: ReactNode;
54
+ labels: ReactNode;
55
+ };
56
+ /** Resolves an edge's endpoints against a box index, dropping any that dangle. */
57
+ declare function wiresFor<E extends EdgeBase>(edges: E[] | undefined, boxes: Map<string, Box>, decorate?: (edge: E) => Partial<Wire>): Wire[];
58
+
59
+ export { type Wire, renderWires, wiresFor };
@@ -0,0 +1,90 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from "./marks.js";
3
+ import { routeEdge, spreadPorts } from "./route.js";
4
+ function renderWires(wires, uid) {
5
+ const uses = [];
6
+ wires.forEach((wire, index) => {
7
+ const { edge } = wire;
8
+ const pinned = wire.keepWaypoints === true && Boolean(edge.via?.length || edge.channelX !== void 0 || edge.channelY !== void 0);
9
+ if (pinned) return;
10
+ if (edge.fromSide) uses.push({ face: `${edge.from}:${edge.fromSide}`, ref: `${index}:from` });
11
+ if (edge.toSide) uses.push({ face: `${edge.to}:${edge.toSide}`, ref: `${index}:to` });
12
+ if (!edge.fromSide) uses.push({ face: `${edge.from}:auto`, ref: `${index}:from` });
13
+ if (!edge.toSide) uses.push({ face: `${edge.to}:auto`, ref: `${index}:to` });
14
+ });
15
+ const offsets = spreadPorts(uses);
16
+ const routed = wires.map((wire, index) => {
17
+ const { edge } = wire;
18
+ const keep = wire.keepWaypoints === true;
19
+ return {
20
+ wire,
21
+ index,
22
+ route: routeEdge({
23
+ from: wire.from,
24
+ to: wire.to,
25
+ fromSide: edge.fromSide,
26
+ toSide: edge.toSide,
27
+ mode: edge.route,
28
+ via: keep ? edge.via : void 0,
29
+ channelX: keep ? edge.channelX : void 0,
30
+ channelY: keep ? edge.channelY : void 0,
31
+ fromOffset: offsets.get(`${index}:from`),
32
+ toOffset: offsets.get(`${index}:to`)
33
+ })
34
+ };
35
+ });
36
+ return {
37
+ paths: /* @__PURE__ */ jsx("g", { "data-diagram-wires": "", children: routed.map(({ wire, index, route }) => /* @__PURE__ */ jsx(
38
+ "path",
39
+ {
40
+ d: route.d,
41
+ fill: "none",
42
+ "data-edge": wire.edge.id,
43
+ markerEnd: `url(#${uid}-${wire.open ? "arrow-open" : edgeMarker(wire.edge.variant)})`,
44
+ className: edgeClasses(wire.edge.variant),
45
+ ...edgeStroke(wire.edge.variant, wire.weight)
46
+ },
47
+ wire.edge.id ?? index
48
+ )) }),
49
+ labels: /* @__PURE__ */ jsx("g", { "data-diagram-wire-labels": "", children: routed.map(
50
+ ({ wire, index, route }) => wire.edge.label ? (
51
+ /* `labelDx` / `labelDy` / `labelAt` are deliberately NOT applied.
52
+ They are collision repairs authored against archify's router,
53
+ measured in the positions THAT router produced — and this one
54
+ does not produce the same collisions, so honouring them moves
55
+ labels off clear ground and onto the nodes they were nudged away
56
+ from. Dropping the nudge and keeping the wording is the reading
57
+ that preserves the meaning; the field stays in the type so a
58
+ specification carrying it still typechecks. */
59
+ /* @__PURE__ */ jsx(
60
+ EdgeLabel,
61
+ {
62
+ x: route.label.x,
63
+ y: route.label.y,
64
+ axis: route.label.axis,
65
+ text: wire.edge.label,
66
+ chip: wire.edge.classification,
67
+ note: wire.edge.note
68
+ },
69
+ wire.edge.id ?? index
70
+ )
71
+ ) : null
72
+ ) })
73
+ };
74
+ }
75
+ function wiresFor(edges, boxes, decorate) {
76
+ if (!edges) return [];
77
+ const out = [];
78
+ for (const edge of edges) {
79
+ const from = boxes.get(edge.from);
80
+ const to = boxes.get(edge.to);
81
+ if (!from || !to) continue;
82
+ out.push({ edge, from, to, ...decorate?.(edge) });
83
+ }
84
+ return out;
85
+ }
86
+ export {
87
+ renderWires,
88
+ wiresFor
89
+ };
90
+ //# sourceMappingURL=wires.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/diagrams/lib/wires.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { EdgeBase, Side, Variant } from '../spec'\nimport type { Box } from './geometry'\nimport { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from './marks'\nimport { routeEdge, spreadPorts, type PortUse } from './route'\n\n/** One line to draw, once its two boxes are known. */\nexport interface Wire {\n edge: EdgeBase & { variant?: Variant | 'return'; classification?: string; note?: string }\n from: Box\n to: Box\n /** Overrides the variant's own stroke weight — a main path drawn heavier. */\n weight?: number\n /** An open arrowhead rather than a filled one. */\n open?: boolean\n /**\n * Whether this line's `via` / `channelX` / `channelY` mean anything here.\n *\n * A waypoint is an ABSOLUTE coordinate in whatever space the author's nodes\n * were placed in. When a specification pins its nodes with `pos`, that space\n * is this renderer's space too and the waypoints land exactly where they were\n * meant to. When the nodes are placed from a lane and a column, this renderer\n * chose the coordinates — so the author's waypoints refer to a grid that does\n * not exist here, and following them drags lines across unrelated nodes.\n *\n * So the rule is per line and not per figure: honour the waypoints when both\n * of a line's endpoints were placed absolutely, and route automatically\n * otherwise. Defaults to false, which is the safe half.\n */\n keepWaypoints?: boolean\n}\n\n/**\n * Every line in a figure, routed together.\n *\n * Together rather than one at a time, because port spreading is a property of a\n * FACE and not of a line: how far off centre a given arrow sits depends on how\n * many others land on the same face, which no single line can know. Routing\n * them independently is what produces three arrowheads stacked on one point.\n *\n * Lines that pin their own geometry — `via`, a channel, an explicit side — opt\n * out of the spread. An author who placed a line has already answered the\n * question the spread exists to answer, and moving it afterwards makes the\n * control useless.\n *\n * Returns the paths and the labels separately because ORDER IS THE MASK: a\n * label's plate has to be painted over every line in the figure, not merely\n * over its own. Interleaving them puts the third line on top of the first\n * line's label.\n */\nexport function renderWires(\n wires: Wire[],\n uid: string,\n): { paths: ReactNode; labels: ReactNode } {\n const uses: PortUse[] = []\n\n wires.forEach((wire, index) => {\n const { edge } = wire\n const pinned =\n wire.keepWaypoints === true &&\n Boolean(edge.via?.length || edge.channelX !== undefined || edge.channelY !== undefined)\n if (pinned) return\n if (edge.fromSide) uses.push({ face: `${edge.from}:${edge.fromSide}`, ref: `${index}:from` })\n if (edge.toSide) uses.push({ face: `${edge.to}:${edge.toSide}`, ref: `${index}:to` })\n if (!edge.fromSide) uses.push({ face: `${edge.from}:auto`, ref: `${index}:from` })\n if (!edge.toSide) uses.push({ face: `${edge.to}:auto`, ref: `${index}:to` })\n })\n\n const offsets = spreadPorts(uses)\n\n const routed = wires.map((wire, index) => {\n const { edge } = wire\n const keep = wire.keepWaypoints === true\n return {\n wire,\n index,\n route: routeEdge({\n from: wire.from,\n to: wire.to,\n fromSide: edge.fromSide as Side | undefined,\n toSide: edge.toSide as Side | undefined,\n mode: edge.route,\n via: keep ? edge.via : undefined,\n channelX: keep ? edge.channelX : undefined,\n channelY: keep ? edge.channelY : undefined,\n fromOffset: offsets.get(`${index}:from`),\n toOffset: offsets.get(`${index}:to`),\n }),\n }\n })\n\n return {\n paths: (\n <g data-diagram-wires=\"\">\n {routed.map(({ wire, index, route }) => (\n <path\n key={wire.edge.id ?? index}\n d={route.d}\n fill=\"none\"\n data-edge={wire.edge.id}\n markerEnd={`url(#${uid}-${wire.open ? 'arrow-open' : edgeMarker(wire.edge.variant)})`}\n className={edgeClasses(wire.edge.variant)}\n {...edgeStroke(wire.edge.variant, wire.weight)}\n />\n ))}\n </g>\n ),\n labels: (\n <g data-diagram-wire-labels=\"\">\n {routed.map(({ wire, index, route }) =>\n wire.edge.label ? (\n /* `labelDx` / `labelDy` / `labelAt` are deliberately NOT applied.\n They are collision repairs authored against archify's router,\n measured in the positions THAT router produced — and this one\n does not produce the same collisions, so honouring them moves\n labels off clear ground and onto the nodes they were nudged away\n from. Dropping the nudge and keeping the wording is the reading\n that preserves the meaning; the field stays in the type so a\n specification carrying it still typechecks. */\n <EdgeLabel\n key={wire.edge.id ?? index}\n x={route.label.x}\n y={route.label.y}\n axis={route.label.axis}\n text={wire.edge.label}\n chip={wire.edge.classification}\n note={wire.edge.note}\n />\n ) : null,\n )}\n </g>\n ),\n }\n}\n\n/** Resolves an edge's endpoints against a box index, dropping any that dangle. */\nexport function wiresFor<E extends EdgeBase>(\n edges: E[] | undefined,\n boxes: Map<string, Box>,\n decorate?: (edge: E) => Partial<Wire>,\n): Wire[] {\n if (!edges) return []\n const out: Wire[] = []\n for (const edge of edges) {\n const from = boxes.get(edge.from)\n const to = boxes.get(edge.to)\n // A relationship naming a node that is not in the figure is a specification\n // error, and drawing it from nowhere to nowhere would hide that. Dropping\n // it leaves the summary list — which iterates the same array — as the place\n // the mistake shows up as a line pointing at an id no node claims.\n if (!from || !to) continue\n out.push({ edge, from, to, ...decorate?.(edge) })\n }\n return out\n}\n"],"mappings":"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","names":[]}