@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.
- package/README.md +46 -0
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1245 -1203
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +1 -1
- package/src/__tests__/ascii-er-attribute-column-order-1134.test.ts +69 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +27 -4
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +13 -3
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +107 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +15 -9
- package/src/__tests__/ascii-sequence-activation.test.ts +171 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +1 -1
- package/src/__tests__/ascii-shape-rounded.test.ts +1 -1
- package/src/__tests__/ascii-shapes-circle.test.ts +1 -1
- package/src/__tests__/ascii-shapes-hexagon.test.ts +1 -1
- package/src/__tests__/ascii-shapes-special.test.ts +35 -0
- package/src/__tests__/ascii-validate.test.ts +1 -42
- package/src/__tests__/ascii.test.ts +1 -1
- package/src/__tests__/testdata/ascii/er_attributes.txt +9 -9
- package/src/__tests__/testdata/ascii/er_identifying.txt +8 -8
- package/src/__tests__/testdata/unicode/er_attributes.txt +9 -9
- package/src/__tests__/testdata/unicode/er_identifying.txt +8 -8
- package/src/{validate.ts → __tests__/validate.ts} +3 -40
- package/src/draw-boxes.ts +14 -0
- package/src/er-diagram.ts +134 -31
- package/src/sequence.ts +87 -1
- package/src/shapes/index.ts +47 -5
- package/src/shapes/circle.ts +0 -31
- package/src/shapes/diamond.ts +0 -31
- package/src/shapes/hexagon.ts +0 -35
- 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
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
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
|
-
|
|
|
15
|
-
+----------------+ |
|
|
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
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│
|
|
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
|
-
│
|
|
15
|
-
└────────────────┘ │
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
48
|
-
return `${
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
|
package/src/shapes/index.ts
CHANGED
|
@@ -16,13 +16,15 @@ import type {
|
|
|
16
16
|
} from './types.ts'
|
|
17
17
|
|
|
18
18
|
// Import all shape renderers
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
package/src/shapes/circle.ts
DELETED
|
@@ -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
|
-
}
|
package/src/shapes/diamond.ts
DELETED
|
@@ -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
|
-
}
|
package/src/shapes/hexagon.ts
DELETED
|
@@ -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
|
-
}
|