@zombie-mermaid/ascii-renderer 2.2.1 → 3.0.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 (33) hide show
  1. package/README.md +46 -0
  2. package/dist/index.cjs +7 -7
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.js +1245 -1203
  5. package/dist/index.js.map +1 -1
  6. package/package.json +9 -4
  7. package/src/__tests__/ascii-decision-lr-box-start.test.ts +1 -1
  8. package/src/__tests__/ascii-er-attribute-column-order-1134.test.ts +69 -0
  9. package/src/__tests__/ascii-er-corner-glyphs.test.ts +27 -4
  10. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +13 -3
  11. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +107 -0
  12. package/src/__tests__/ascii-er-stray-connectors.test.ts +15 -9
  13. package/src/__tests__/ascii-sequence-activation.test.ts +171 -0
  14. package/src/__tests__/ascii-shape-diamond.test.ts +1 -1
  15. package/src/__tests__/ascii-shape-rounded.test.ts +1 -1
  16. package/src/__tests__/ascii-shapes-circle.test.ts +1 -1
  17. package/src/__tests__/ascii-shapes-hexagon.test.ts +1 -1
  18. package/src/__tests__/ascii-shapes-special.test.ts +35 -0
  19. package/src/__tests__/ascii-validate.test.ts +1 -42
  20. package/src/__tests__/ascii.test.ts +1 -1
  21. package/src/__tests__/testdata/ascii/er_attributes.txt +9 -9
  22. package/src/__tests__/testdata/ascii/er_identifying.txt +8 -8
  23. package/src/__tests__/testdata/unicode/er_attributes.txt +9 -9
  24. package/src/__tests__/testdata/unicode/er_identifying.txt +8 -8
  25. package/src/{validate.ts → __tests__/validate.ts} +3 -40
  26. package/src/draw-boxes.ts +14 -0
  27. package/src/er-diagram.ts +134 -31
  28. package/src/sequence.ts +87 -1
  29. package/src/shapes/index.ts +47 -5
  30. package/src/shapes/circle.ts +0 -31
  31. package/src/shapes/diamond.ts +0 -31
  32. package/src/shapes/hexagon.ts +0 -35
  33. package/src/shapes/rounded.ts +0 -31
@@ -10,12 +10,12 @@ erDiagram
10
10
  }
11
11
  CUSTOMER ||--o{ ORDER : places
12
12
  ---
13
- +-----------------+ +------------------+
14
- | CUSTOMER | | ORDER |
15
- +-----------------+ +------------------+
16
- | PK string name |-|----o<| PK int id |
17
- | UK string email | places | string status |
18
- | int age | +------------------+
19
- +-----------------+
20
-
21
-
13
+ +-----------------+ +---------------+
14
+ | CUSTOMER | | ORDER |
15
+ +-----------------+ +---------------+
16
+ | string name PK |-|----o<| int id PK |
17
+ | string email UK | places | string status |
18
+ | int age | +---------------+
19
+ +-----------------+
20
+
21
+
@@ -8,11 +8,11 @@ erDiagram
8
8
  string city
9
9
  }
10
10
  ---
11
- +----------------+ +------------------+
12
- | PERSON | | ADDRESS |
13
- +----------------+-|------o<+------------------+
14
- | PK string name | lives_at | string street |
15
- +----------------+ | string city |
16
- +------------------+
17
-
18
-
11
+ +----------------+ +---------------+
12
+ | PERSON | | ADDRESS |
13
+ +----------------+-|------o<+---------------+
14
+ | string name PK | lives_at | string street |
15
+ +----------------+ | string city |
16
+ +---------------+
17
+
18
+
@@ -10,12 +10,12 @@ erDiagram
10
10
  }
11
11
  CUSTOMER ||--o{ ORDER : places
12
12
  ---
13
- ┌─────────────────┐ ┌──────────────────┐
14
- │ CUSTOMER │ │ ORDER │
15
- ├─────────────────┤ ├──────────────────┤
16
- │ PK string name │─│────○╟│ PK int id │
17
- │ UK string email │ places │ string status │
18
- │ int age │ └──────────────────┘
19
- └─────────────────┘
20
-
21
-
13
+ ┌─────────────────┐ ┌───────────────┐
14
+ │ CUSTOMER │ │ ORDER │
15
+ ├─────────────────┤ ├───────────────┤
16
+ │ string name PK │─│────○╟│ int id PK │
17
+ │ string email UK │ places │ string status │
18
+ │ int age │ └───────────────┘
19
+ └─────────────────┘
20
+
21
+
@@ -8,11 +8,11 @@ erDiagram
8
8
  string city
9
9
  }
10
10
  ---
11
- ┌────────────────┐ ┌──────────────────┐
12
- │ PERSON │ │ ADDRESS │
13
- ├────────────────┤─│──────○╟├──────────────────┤
14
- │ PK string name │ lives_at │ string street │
15
- └────────────────┘ │ string city │
16
- └──────────────────┘
17
-
18
-
11
+ ┌────────────────┐ ┌───────────────┐
12
+ │ PERSON │ │ ADDRESS │
13
+ ├────────────────┤─│──────○╟├───────────────┤
14
+ │ string name PK │ lives_at │ string street │
15
+ └────────────────┘ │ string city │
16
+ └───────────────┘
17
+
18
+
@@ -1,8 +1,7 @@
1
1
  /**
2
- * ASCII Rendering Validation Utilities
3
- *
4
- * Provides validation functions for ASCII diagram output,
5
- * including diagonal line detection to ensure orthogonal-only routing.
2
+ * ASCII rendering validation helpers used by the test suite to sanity-check
3
+ * rendered output (diagonal-line and orphaned-junction detection).
4
+ * Test-only — not part of the package's public render path.
6
5
  */
7
6
 
8
7
  /**
@@ -100,32 +99,6 @@ export function findDiagonalLines(asciiOutput: string): DiagonalPosition[] {
100
99
  return positions
101
100
  }
102
101
 
103
- /**
104
- * Assert that ASCII output contains no diagonal lines.
105
- * Throws an error with detailed position information if diagonals are found.
106
- *
107
- * @param asciiOutput - The rendered ASCII diagram string
108
- * @param context - Optional context string for error message (e.g., diagram name)
109
- * @throws Error if diagonal characters are present
110
- */
111
- export function assertNoDiagonals(asciiOutput: string, context?: string): void {
112
- if (!hasDiagonalLines(asciiOutput)) {
113
- return
114
- }
115
-
116
- const positions = findDiagonalLines(asciiOutput)
117
- const contextStr = context ? ` in "${context}"` : ''
118
- const positionStr = positions
119
- .map((p) => ` Line ${p.line}, Col ${p.col}: '${p.char}'`)
120
- .join('\n')
121
-
122
- throw new Error(
123
- `Diagonal lines detected${contextStr}. ` +
124
- `Edges must use orthogonal Manhattan routing (90° bends only).\n` +
125
- `Found ${positions.length} diagonal character(s):\n${positionStr}`,
126
- )
127
- }
128
-
129
102
  /**
130
103
  * Position of an orphaned tee/junction character in ASCII output.
131
104
  */
@@ -177,13 +150,3 @@ export function findOrphanedJunctions(
177
150
 
178
151
  return positions
179
152
  }
180
-
181
- /**
182
- * Check if ASCII output contains any orphaned tee/junction characters.
183
- * Returns true if any are found (which is an error condition).
184
- *
185
- * @param asciiOutput - The rendered ASCII diagram string
186
- */
187
- export function hasOrphanedJunctions(asciiOutput: string): boolean {
188
- return findOrphanedJunctions(asciiOutput).length > 0
189
- }
package/src/draw-boxes.ts CHANGED
@@ -99,6 +99,20 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
99
99
  write(box, from.x, to.y, effectiveCorners.bl)
100
100
  write(box, to.x, to.y, effectiveCorners.br)
101
101
 
102
+ // Cylinder (database) gets a rim line just inside its top and bottom
103
+ // border — the ellipse-pinch that makes it read as a cylinder rather than
104
+ // a plain rounded box, which otherwise uses the identical corner glyphs
105
+ // (see corners.ts's SHAPE_CORNERS.cylinder). getShapeDimensions already
106
+ // reserves the extra 2 rows of height this needs (see the cylinder branch
107
+ // of shapes/special.ts's getDimensions) — issue #1119.
108
+ if (node.shape === 'cylinder' && to.y - from.y >= 4) {
109
+ for (const rimY of [from.y + 1, to.y - 1]) {
110
+ for (let x = from.x + 1; x < to.x; x++) write(box, x, rimY, hChar)
111
+ write(box, from.x, rimY, vChar)
112
+ write(box, to.x, rimY, vChar)
113
+ }
114
+ }
115
+
102
116
  // Center the multi-line display label inside the box
103
117
  const label = node.displayLabel
104
118
  const lines = splitLines(label)
package/src/er-diagram.ts CHANGED
@@ -42,10 +42,22 @@ import { DEFAULT_PADDING_X, DEFAULT_PADDING_Y, paddingOffset } from './types.ts'
42
42
  // Entity box content
43
43
  // ============================================================================
44
44
 
45
- /** Format an attribute line: "PK type name" or "FK type name" etc. */
45
+ /**
46
+ * Format an attribute line as "type name" or "type name PK" etc.
47
+ *
48
+ * Real mermaid's SVG lays attribute columns out left-to-right as
49
+ * type/name/keys (confirmed against `attribute-type`/`attribute-name`/
50
+ * `attribute-keys` x-coordinates in the form-judge report on issue #1119) —
51
+ * so the key column trails here too, rather than leading with a fixed-width
52
+ * prefix like the pre-#1134 "PK type name" order did. Nothing else in this
53
+ * file assumes a fixed-width key column: box width is just the widest
54
+ * rendered line (see measureMultiBox in draw-boxes.ts), so a trailing,
55
+ * unpadded key suffix sizes the box correctly with no extra alignment logic
56
+ * needed.
57
+ */
46
58
  function formatAttribute(attr: ErAttribute): string {
47
- const keyStr = attr.keys.length > 0 ? attr.keys.join(',') + ' ' : ' '
48
- return `${keyStr}${attr.type} ${attr.name}`
59
+ const keySuffix = attr.keys.length > 0 ? ' ' + attr.keys.join(',') : ''
60
+ return `${attr.type} ${attr.name}${keySuffix}`
49
61
  }
50
62
 
51
63
  /** Build sections for an entity box: [header], [attributes] */
@@ -532,6 +544,7 @@ export function renderErAscii(
532
544
  y: number,
533
545
  minX: number,
534
546
  maxX: number,
547
+ avoidForeignLines: boolean,
535
548
  ): boolean {
536
549
  for (let i = 0; i < cells.length; i++) {
537
550
  const x = startX + i
@@ -539,6 +552,31 @@ export function renderErAscii(
539
552
  if (isProtected(x, y) || rc[x]?.[y] === 'arrow') return false
540
553
  if (boxCells.has(`${x},${y}`)) return false
541
554
  }
555
+ if (!avoidForeignLines) return true
556
+ // A cell already holding a *different* relationship's line reads as
557
+ // free above (isProtected only tracks text/border), so without this
558
+ // check the search below would happily park this relationship's label
559
+ // right beside someone else's line — visually implying the label
560
+ // describes that line's solid/dashed style instead of its own (issue
561
+ // #1119). currentRelLineCells (see setCGuarded) is this relationship's
562
+ // own line cells, drawn moments earlier in this same iteration — those
563
+ // stay allowed, matching the "label wins over the line beneath it" doc
564
+ // comment above. Checked a cell wider than the label's own text on each
565
+ // side (not just cells.length) so a foreign line stem passing just
566
+ // outside the label's own span doesn't end up flush against it either —
567
+ // narrower than that and the existing single-cell post-placement
568
+ // cleanup below can't fully clear it. Only enforced on this first
569
+ // (strict) search pass — see the two-pass loop below — a dense diagram
570
+ // with no foreign-line-free row anywhere in range falls back to the
571
+ // old, permissive check rather than dropping the label outright (a
572
+ // worse information loss than the visual ambiguity this guards
573
+ // against).
574
+ for (let x = startX - 2; x <= startX + cells.length; x++) {
575
+ if (x < minX - 2 || x > maxX + 1) continue
576
+ if (rc[x]?.[y] === 'line' && !currentRelLineCells.has(`${x},${y}`)) {
577
+ return false
578
+ }
579
+ }
542
580
  return true
543
581
  }
544
582
 
@@ -595,7 +633,38 @@ export function renderErAscii(
595
633
  // the same silent-corruption shape as issue #350, just between two
596
634
  // relationships instead of a relationship and a box.
597
635
  if (role !== 'text' && rc[x]?.[y] === 'text') return
636
+ // A same-family crossing (solid-over-solid, dashed-over-dashed, or
637
+ // either meeting a direction-agnostic corner/marker glyph — see
638
+ // lineStyleFamily) is always allowed to overwrite: two relationship
639
+ // lines crossing is normal, expected ER layout (see the doc comment
640
+ // above), and same-style glyphs look identical regardless of which
641
+ // relationship drew them. A *dashed* write landing on an already-drawn
642
+ // *solid* glyph is the one combination refused: without this, whichever
643
+ // relationship simply happened to be declared (and so drawn) later
644
+ // always won a shared cell regardless of which one it visually belongs
645
+ // to, so an identifying relationship's own solid line could end up
646
+ // showing a dashed glyph purely from draw order when it shared layout
647
+ // space with a non-identifying relationship (issue #1145). Solid takes
648
+ // precedence over dashed wherever they'd otherwise collide — not
649
+ // "whichever wrote first", which would just swap one order dependency
650
+ // for another — so the outcome is the same regardless of which
651
+ // relationship is declared first, mirroring how a label always wins
652
+ // over the line beneath it (the isProtected/canPlaceLabelLine guards
653
+ // above): a solid write always goes through, including over an
654
+ // existing dashed glyph.
655
+ if (
656
+ lineStyleFamily(ch) === 'dashed' &&
657
+ lineStyleFamily(canvas[x]?.[y]) === 'solid'
658
+ ) {
659
+ return
660
+ }
598
661
  setC(x, y, ch, role)
662
+ // Track this relationship's own line cells (see currentRelLineCells,
663
+ // reset per relationship below) so a later label-placement search can
664
+ // tell "sits on my own line" (fine) apart from "sits on a *different*
665
+ // relationship's line" (issue #1119 — a relocated label landing next to
666
+ // an unrelated line reads as describing that line's style instead).
667
+ if (role === 'line') currentRelLineCells.add(`${x},${y}`)
599
668
  }
600
669
 
601
670
  /**
@@ -710,6 +779,25 @@ export function renderErAscii(
710
779
  const dashH = useAscii ? '.' : '╌'
711
780
  const dashV = useAscii ? ':' : '┊'
712
781
 
782
+ /**
783
+ * Family a straight relationship-line glyph belongs to — 'solid' for an
784
+ * identifying relationship's H/V glyphs, 'dashed' for a non-identifying
785
+ * relationship's dashH/dashV glyphs, or undefined for anything else
786
+ * (corner glyphs from getCornerChar, crow's-foot markers, box borders,
787
+ * label text). Corners and markers are direction/role glyphs, not style
788
+ * glyphs — they look identical regardless of which relationship's style
789
+ * drew them, so they're deliberately excluded here rather than folded
790
+ * into either family; see setCGuarded's use of this for why that
791
+ * exclusion matters (issue #1145).
792
+ */
793
+ function lineStyleFamily(
794
+ ch: string | undefined,
795
+ ): 'solid' | 'dashed' | undefined {
796
+ if (ch === H || ch === V) return 'solid'
797
+ if (ch === dashH || ch === dashV) return 'dashed'
798
+ return undefined
799
+ }
800
+
713
801
  /**
714
802
  * Character for the single cell where a routed relationship's path turns
715
803
  * — a vertical segment and a horizontal segment meeting at a right angle
@@ -822,7 +910,12 @@ export function renderErAscii(
822
910
  return entity.x + margin + Math.round(idx * step)
823
911
  }
824
912
 
913
+ // This relationship's own line cells, reset each iteration — see
914
+ // setCGuarded and canPlaceLabelLineAvoidingMarkers above.
915
+ let currentRelLineCells = new Set<string>()
916
+
825
917
  for (const rel of diagram.relationships) {
918
+ currentRelLineCells = new Set<string>()
826
919
  const e1 = placed.get(rel.entity1)
827
920
  const e2 = placed.get(rel.entity2)
828
921
  if (!e1 || !e2) continue
@@ -1419,37 +1512,47 @@ export function renderErAscii(
1419
1512
  // legitimately run its label past a box that never touched
1420
1513
  // startY/endY's naive band (#350). It also means clear of markers,
1421
1514
  // not just text/borders (#392) — before giving up.
1515
+ // Two passes: first strict (also dodging a *different*
1516
+ // relationship's own line — #1119), then permissive (the pre-#1119
1517
+ // check) if nothing in range clears the strict bar. A dense diagram
1518
+ // where every candidate row touches some other relationship's line
1519
+ // would otherwise drop the label entirely — worse than the visual
1520
+ // ambiguity the strict pass exists to avoid.
1422
1521
  let placedAtY: number | null = null
1423
- for (
1424
- let offset = 0;
1425
- offset <= endY - startY && placedAtY === null;
1426
- offset++
1427
- ) {
1428
- const candidates =
1429
- offset === 0
1430
- ? [naturalStartY]
1431
- : [naturalStartY + offset, naturalStartY - offset]
1432
- for (const candidateStart of candidates) {
1433
- if (
1434
- candidateStart < startY ||
1435
- candidateStart + blockHeight - 1 > endY
1436
- ) {
1437
- continue
1438
- }
1439
- const fits = cellsPerLine.every((cells, lineIdx) =>
1440
- canPlaceLabelLineAvoidingMarkers(
1441
- cells,
1442
- labelX,
1443
- candidateStart + lineIdx,
1444
- 0,
1445
- lastLx,
1446
- ),
1447
- )
1448
- if (fits) {
1449
- placedAtY = candidateStart
1450
- break
1522
+ for (const avoidForeignLines of [true, false]) {
1523
+ for (
1524
+ let offset = 0;
1525
+ offset <= endY - startY && placedAtY === null;
1526
+ offset++
1527
+ ) {
1528
+ const candidates =
1529
+ offset === 0
1530
+ ? [naturalStartY]
1531
+ : [naturalStartY + offset, naturalStartY - offset]
1532
+ for (const candidateStart of candidates) {
1533
+ if (
1534
+ candidateStart < startY ||
1535
+ candidateStart + blockHeight - 1 > endY
1536
+ ) {
1537
+ continue
1538
+ }
1539
+ const fits = cellsPerLine.every((cells, lineIdx) =>
1540
+ canPlaceLabelLineAvoidingMarkers(
1541
+ cells,
1542
+ labelX,
1543
+ candidateStart + lineIdx,
1544
+ 0,
1545
+ lastLx,
1546
+ avoidForeignLines,
1547
+ ),
1548
+ )
1549
+ if (fits) {
1550
+ placedAtY = candidateStart
1551
+ break
1552
+ }
1451
1553
  }
1452
1554
  }
1555
+ if (placedAtY !== null) break
1453
1556
  }
1454
1557
 
1455
1558
  if (placedAtY !== null) {
package/src/sequence.ts CHANGED
@@ -96,6 +96,12 @@ export function renderSequenceAscii(
96
96
  const JB = useAscii ? '+' : '┴' // bottom junction on lifeline
97
97
  const JL = useAscii ? '+' : '├' // left junction
98
98
  const JR = useAscii ? '+' : '┤' // right junction
99
+ // Activation bar: the double-line glyph a lifeline gets while a
100
+ // participant is actively processing a call (issue #1133), overlaid in
101
+ // place of the plain lifeline `V`. Same Unicode/ASCII pairing draw-boxes.ts
102
+ // already uses for a double border (`║`/`‖` — see its `isDoubleBox`
103
+ // branch), reused here rather than inventing a second one.
104
+ const ACT = useAscii ? '‖' : '║'
99
105
 
100
106
  // ---- LAYOUT: compute lifeline X positions ----
101
107
 
@@ -850,10 +856,90 @@ export function renderSequenceAscii(
850
856
  }
851
857
  const lifelineBottom = (i: number) => destroyRow.get(i) ?? footerY
852
858
 
859
+ // ---- LAYOUT: activation intervals (rows where a lifeline is "active") ----
860
+ //
861
+ // Ports svg-renderer's layout.ts activation algorithm (its
862
+ // startActivation/endActivation/applyActivationEvents, ~lines 288-345) to
863
+ // row indices instead of y-pixels: a per-actor LIFO stack of open
864
+ // activations (start row pushed on `activate`, popped and closed into an
865
+ // interval on the matching `deactivate`), fed by both the standalone
866
+ // `activations` list and each message's inline `activate`/`deactivate`,
867
+ // plus a final pass closing anything still open at diagram end
868
+ // (unbalanced input) — same as the SVG layout's closing pass.
869
+ //
870
+ // Nesting depth (the SVG renderer's `depth`, used there to horizontally
871
+ // offset nested activation rects) isn't tracked here: the stack's LIFO
872
+ // order alone is enough to pair activate/deactivate correctly, and
873
+ // `isActiveAt` below only distinguishes "some activation covers this row"
874
+ // from "idle" — see the ACT-glyph comment there for why nesting doesn't
875
+ // get its own glyph.
876
+ const activationIntervals = new Map<
877
+ string,
878
+ Array<{ topRow: number; bottomRow: number }>
879
+ >()
880
+ const activationStacks = new Map<string, number[]>()
881
+
882
+ function startActivation(actorId: string, row: number): void {
883
+ const stack = activationStacks.get(actorId) ?? []
884
+ activationStacks.set(actorId, stack)
885
+ stack.push(row)
886
+ }
887
+ function endActivation(actorId: string, row: number): void {
888
+ const stack = activationStacks.get(actorId)
889
+ const startRow = stack?.pop()
890
+ if (startRow === undefined) return
891
+ const list = activationIntervals.get(actorId) ?? []
892
+ activationIntervals.set(actorId, list)
893
+ list.push({ topRow: startRow, bottomRow: row })
894
+ }
895
+
896
+ const activationEventsByAfterIndex = new Map<
897
+ number,
898
+ typeof diagram.activations
899
+ >()
900
+ for (const event of diagram.activations) {
901
+ const list = activationEventsByAfterIndex.get(event.afterIndex) ?? []
902
+ list.push(event)
903
+ activationEventsByAfterIndex.set(event.afterIndex, list)
904
+ }
905
+ function applyActivationEvents(afterIndex: number, row: number): void {
906
+ for (const event of activationEventsByAfterIndex.get(afterIndex) ?? []) {
907
+ if (event.kind === 'start') startActivation(event.actorId, row)
908
+ else endActivation(event.actorId, row)
909
+ }
910
+ }
911
+
912
+ applyActivationEvents(-1, headerBottom)
913
+ for (let m = 0; m < diagram.messages.length; m++) {
914
+ const msg = diagram.messages[m]!
915
+ const row = msgArrowY[m]!
916
+ if (msg.activate) startActivation(msg.to, row)
917
+ if (msg.deactivate) endActivation(msg.from, row)
918
+ applyActivationEvents(m, row)
919
+ }
920
+ // Unbalanced `activate` with no matching `deactivate`: close at the
921
+ // actor's own lifeline end, mirroring svg-renderer's layout.ts closing
922
+ // pass at the bottom of the message area.
923
+ for (const [actorId, stack] of activationStacks) {
924
+ if (stack.length === 0) continue
925
+ const bottomRow = lifelineBottom(actorIndexOf(actorId))
926
+ const list = activationIntervals.get(actorId) ?? []
927
+ activationIntervals.set(actorId, list)
928
+ for (const startRow of stack) list.push({ topRow: startRow, bottomRow })
929
+ }
930
+
931
+ /** Whether `actorId`'s lifeline is inside any activation at row `y`. */
932
+ function isActiveAt(actorId: string, y: number): boolean {
933
+ const intervals = activationIntervals.get(actorId)
934
+ if (!intervals) return false
935
+ return intervals.some((iv) => y >= iv.topRow && y <= iv.bottomRow)
936
+ }
937
+
853
938
  for (let i = 0; i < diagram.actors.length; i++) {
854
939
  const x = llX[i]!
940
+ const actorId = diagram.actors[i]!.id
855
941
  for (let y = lifelineTop(i); y <= lifelineBottom(i); y++) {
856
- setC(x, y, V, 'line')
942
+ setC(x, y, isActiveAt(actorId, y) ? ACT : V, 'line')
857
943
  }
858
944
  }
859
945
 
@@ -16,13 +16,15 @@ import type {
16
16
  } from './types.ts'
17
17
 
18
18
  // Import all shape renderers
19
- import { rectangleRenderer } from './rectangle.ts'
20
- import { diamondRenderer } from './diamond.ts'
21
- import { circleRenderer } from './circle.ts'
19
+ import {
20
+ rectangleRenderer,
21
+ getBoxDimensions,
22
+ renderBox,
23
+ getBoxAttachmentPoint,
24
+ } from './rectangle.ts'
25
+ import { getCorners } from './corners.ts'
22
26
  import { stateStartRenderer, stateEndRenderer } from './state.ts'
23
- import { roundedRenderer } from './rounded.ts'
24
27
  import { stadiumRenderer } from './stadium.ts'
25
- import { hexagonRenderer } from './hexagon.ts'
26
28
  import {
27
29
  subroutineRenderer,
28
30
  doublecircleRenderer,
@@ -42,6 +44,46 @@ export type {
42
44
  ShapeRegistry,
43
45
  }
44
46
 
47
+ /**
48
+ * Corner-decorated shape renderers — circle, diamond, rounded, hexagon.
49
+ * Each one renders a rectangle (`getBoxDimensions`/`renderBox`/
50
+ * `getBoxAttachmentPoint`) with a different corner-marker set pulled from
51
+ * corners.ts's `SHAPE_CORNERS`, instead of drawing curves or diagonals:
52
+ *
53
+ * circle (◯) — circle markers indicate circular shape semantics
54
+ * diamond (◇) — diamond markers indicate decision node semantics
55
+ * rounded (╭╮╰╯) — rounded markers indicate soft edges
56
+ * hexagon (⌜⌝⌞⌟) — crop-corner markers indicate process node semantics;
57
+ * not the hexagon glyph ⬡ (U+2B21) earlier docs here used to claim —
58
+ * JetBrains Mono NL has no glyph for ⬡ at all, and the renderer hasn't
59
+ * actually emitted it since corners.ts moved to the monospace-safe
60
+ * crop-corner style. See issue #1062.
61
+ *
62
+ * Each renders as (TL/TR/BL/BR = that shape's four corner markers above):
63
+ * TL─────────TR
64
+ * │ Label │
65
+ * BL─────────BR
66
+ */
67
+ function createCornerShapeRenderer(
68
+ shape: 'circle' | 'diamond' | 'rounded' | 'hexagon',
69
+ ): ShapeRenderer {
70
+ return {
71
+ getDimensions: getBoxDimensions,
72
+
73
+ render(label, dimensions, options) {
74
+ const corners = getCorners(shape, options.useAscii)
75
+ return renderBox(label, dimensions, corners, options.useAscii)
76
+ },
77
+
78
+ getAttachmentPoint: getBoxAttachmentPoint,
79
+ }
80
+ }
81
+
82
+ export const circleRenderer = createCornerShapeRenderer('circle')
83
+ export const diamondRenderer = createCornerShapeRenderer('diamond')
84
+ export const roundedRenderer = createCornerShapeRenderer('rounded')
85
+ export const hexagonRenderer = createCornerShapeRenderer('hexagon')
86
+
45
87
  /**
46
88
  * Global shape registry — maps shape types to their renderers.
47
89
  * Rectangle is the default fallback for unregistered shapes.
@@ -1,31 +0,0 @@
1
- // ============================================================================
2
- // Circle shape renderer — uses corner decorators instead of curves
3
- // ============================================================================
4
-
5
- import type { ShapeRenderer } from './types.ts'
6
- import {
7
- getBoxDimensions,
8
- renderBox,
9
- getBoxAttachmentPoint,
10
- } from './rectangle.ts'
11
- import { getCorners } from './corners.ts'
12
-
13
- /**
14
- * Circle shape renderer.
15
- * Uses circle markers (◯) at corners to indicate circular shape semantics.
16
- *
17
- * Renders as:
18
- * ◯─────────◯
19
- * │ Label │
20
- * ◯─────────◯
21
- */
22
- export const circleRenderer: ShapeRenderer = {
23
- getDimensions: getBoxDimensions,
24
-
25
- render(label, dimensions, options) {
26
- const corners = getCorners('circle', options.useAscii)
27
- return renderBox(label, dimensions, corners, options.useAscii)
28
- },
29
-
30
- getAttachmentPoint: getBoxAttachmentPoint,
31
- }
@@ -1,31 +0,0 @@
1
- // ============================================================================
2
- // Diamond shape renderer — uses corner decorators instead of diagonals
3
- // ============================================================================
4
-
5
- import type { ShapeRenderer } from './types.ts'
6
- import {
7
- getBoxDimensions,
8
- renderBox,
9
- getBoxAttachmentPoint,
10
- } from './rectangle.ts'
11
- import { getCorners } from './corners.ts'
12
-
13
- /**
14
- * Diamond shape renderer.
15
- * Uses diamond markers (◇) at corners to indicate decision node semantics.
16
- *
17
- * Renders as:
18
- * ◇─────────◇
19
- * │ Label │
20
- * ◇─────────◇
21
- */
22
- export const diamondRenderer: ShapeRenderer = {
23
- getDimensions: getBoxDimensions,
24
-
25
- render(label, dimensions, options) {
26
- const corners = getCorners('diamond', options.useAscii)
27
- return renderBox(label, dimensions, corners, options.useAscii)
28
- },
29
-
30
- getAttachmentPoint: getBoxAttachmentPoint,
31
- }
@@ -1,35 +0,0 @@
1
- // ============================================================================
2
- // Hexagon shape renderer — uses corner decorators instead of diagonals
3
- // ============================================================================
4
-
5
- import type { ShapeRenderer } from './types.ts'
6
- import {
7
- getBoxDimensions,
8
- renderBox,
9
- getBoxAttachmentPoint,
10
- } from './rectangle.ts'
11
- import { getCorners } from './corners.ts'
12
-
13
- /**
14
- * Hexagon shape renderer.
15
- * Uses crop-corner markers (⌜⌝⌞⌟, see corners.ts's `SHAPE_CORNERS.hexagon`)
16
- * to indicate process node semantics — not the hexagon glyph ⬡ (U+2B21)
17
- * this comment used to claim: JetBrains Mono NL has no glyph for ⬡ at all,
18
- * and the renderer hasn't actually emitted it since corners.ts moved to
19
- * the monospace-safe crop-corner style. See issue #1062.
20
- *
21
- * Renders as:
22
- * ⌜─────────⌝
23
- * │ Label │
24
- * ⌞─────────⌟
25
- */
26
- export const hexagonRenderer: ShapeRenderer = {
27
- getDimensions: getBoxDimensions,
28
-
29
- render(label, dimensions, options) {
30
- const corners = getCorners('hexagon', options.useAscii)
31
- return renderBox(label, dimensions, corners, options.useAscii)
32
- },
33
-
34
- getAttachmentPoint: getBoxAttachmentPoint,
35
- }