@oh-my-pi/pi-utils 16.0.7 → 16.0.9

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 (87) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/mermaid-ascii.d.ts +1 -1
  3. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  4. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  5. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  6. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  7. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  8. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  9. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  10. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  11. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  12. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  13. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  14. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  15. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  16. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  17. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  18. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  19. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  20. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  21. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  22. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  23. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  24. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  25. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  26. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  27. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  28. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  29. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  30. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  31. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  32. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  33. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  34. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  35. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  36. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  37. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  38. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  39. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  40. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  41. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  42. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  43. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  44. package/package.json +2 -3
  45. package/src/mermaid-ascii.ts +1 -1
  46. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  47. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  48. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  49. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  50. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  51. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  52. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  53. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  54. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  55. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  56. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  57. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  58. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
  59. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  60. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  61. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  62. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  63. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  64. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  65. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  66. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  67. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  68. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  69. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  70. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  71. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  72. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  73. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  74. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  75. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  76. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  77. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  78. package/src/vendor/mermaid-ascii/index.ts +14 -0
  79. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  80. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  81. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  82. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  83. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  84. package/src/vendor/mermaid-ascii/types.ts +164 -0
  85. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  86. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  87. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
@@ -0,0 +1,101 @@
1
+ // ============================================================================
2
+ // Shape registry — pluggable ASCII shape renderers
3
+ // ============================================================================
4
+
5
+ import type { AsciiNodeShape, Canvas, DrawingCoord, Direction } from '../types'
6
+ import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry } from './types'
7
+
8
+ // Import all shape renderers
9
+ import { rectangleRenderer } from './rectangle'
10
+ import { diamondRenderer } from './diamond'
11
+ import { circleRenderer } from './circle'
12
+ import { stateStartRenderer, stateEndRenderer } from './state'
13
+ import { roundedRenderer } from './rounded'
14
+ import { stadiumRenderer } from './stadium'
15
+ import { hexagonRenderer } from './hexagon'
16
+ import {
17
+ subroutineRenderer,
18
+ doublecircleRenderer,
19
+ cylinderRenderer,
20
+ asymmetricRenderer,
21
+ trapezoidRenderer,
22
+ trapezoidAltRenderer,
23
+ } from './special'
24
+
25
+ // Re-export types
26
+ export type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry }
27
+
28
+ /**
29
+ * Global shape registry — maps shape types to their renderers.
30
+ * Rectangle is the default fallback for unregistered shapes.
31
+ */
32
+ export const shapeRegistry: ShapeRegistry = new Map<AsciiNodeShape, ShapeRenderer>([
33
+ // Core shapes
34
+ ['rectangle', rectangleRenderer],
35
+ ['rounded', roundedRenderer],
36
+ ['diamond', diamondRenderer],
37
+ ['stadium', stadiumRenderer],
38
+ ['circle', circleRenderer],
39
+
40
+ // Batch 1 additions
41
+ ['subroutine', subroutineRenderer],
42
+ ['doublecircle', doublecircleRenderer],
43
+ ['hexagon', hexagonRenderer],
44
+
45
+ // Batch 2 additions
46
+ ['cylinder', cylinderRenderer],
47
+ ['asymmetric', asymmetricRenderer],
48
+ ['trapezoid', trapezoidRenderer],
49
+ ['trapezoid-alt', trapezoidAltRenderer],
50
+
51
+ // State diagram pseudo-states
52
+ ['state-start', stateStartRenderer],
53
+ ['state-end', stateEndRenderer],
54
+ ])
55
+
56
+ /**
57
+ * Get the renderer for a shape type, falling back to rectangle.
58
+ */
59
+ export function getShapeRenderer(shape: AsciiNodeShape): ShapeRenderer {
60
+ return shapeRegistry.get(shape) ?? rectangleRenderer
61
+ }
62
+
63
+ /**
64
+ * Render a node shape to a canvas.
65
+ * This is the main entry point for shape rendering.
66
+ */
67
+ export function renderShape(
68
+ shape: AsciiNodeShape,
69
+ label: string,
70
+ options: ShapeRenderOptions
71
+ ): Canvas {
72
+ const renderer = getShapeRenderer(shape)
73
+ const dimensions = renderer.getDimensions(label, options)
74
+ return renderer.render(label, dimensions, options)
75
+ }
76
+
77
+ /**
78
+ * Get dimensions for a shape given a label.
79
+ * Used during layout to determine node size.
80
+ */
81
+ export function getShapeDimensions(
82
+ shape: AsciiNodeShape,
83
+ label: string,
84
+ options: ShapeRenderOptions
85
+ ): ShapeDimensions {
86
+ const renderer = getShapeRenderer(shape)
87
+ return renderer.getDimensions(label, options)
88
+ }
89
+
90
+ /**
91
+ * Get edge attachment point for a shape.
92
+ */
93
+ export function getShapeAttachmentPoint(
94
+ shape: AsciiNodeShape,
95
+ dir: Direction,
96
+ dimensions: ShapeDimensions,
97
+ baseCoord: DrawingCoord
98
+ ): DrawingCoord {
99
+ const renderer = getShapeRenderer(shape)
100
+ return renderer.getAttachmentPoint(dir, dimensions, baseCoord)
101
+ }
@@ -0,0 +1,175 @@
1
+ // ============================================================================
2
+ // Rectangle shape renderer — standard box with corners
3
+ // ============================================================================
4
+ //
5
+ // This module provides the base box rendering used by all rectangular shapes.
6
+ // The renderBox() function accepts custom corner characters, allowing different
7
+ // shapes to reuse the same rendering logic with different visual markers.
8
+
9
+ import type { Canvas, DrawingCoord, Direction } from '../types'
10
+ import { Up, Down, Left, Right, UpperLeft, UpperRight, LowerLeft, LowerRight, Middle } from '../types'
11
+ import { mkCanvas } from '../canvas'
12
+ import { splitLines } from '../multiline-utils'
13
+ import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types'
14
+ import { dirEquals } from '../edge-routing'
15
+ import { type CornerChars, getCorners } from './corners'
16
+ import { displayWidth, toCells } from '../../text-metrics'
17
+
18
+ // ============================================================================
19
+ // Shared dimension calculation
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Calculate standard box dimensions for any rectangular shape.
24
+ * Used by rectangle, circle, diamond, hexagon, etc.
25
+ */
26
+ export function getBoxDimensions(label: string, options: ShapeRenderOptions): ShapeDimensions {
27
+ const lines = splitLines(label)
28
+ const maxLineWidth = Math.max(...lines.map(l => displayWidth(l)), 0)
29
+ const lineCount = lines.length
30
+
31
+ // Width: 2*padding + maxLineWidth + 2 border chars
32
+ const innerWidth = 2 * options.padding + maxLineWidth
33
+ const width = innerWidth + 2
34
+
35
+ // Height: lineCount + 2*padding + 2 border chars
36
+ // Ensure innerHeight is odd for symmetric vertical centering
37
+ const rawInnerHeight = lineCount + 2 * options.padding
38
+ const innerHeight = rawInnerHeight % 2 === 0 ? rawInnerHeight + 1 : rawInnerHeight
39
+ const height = innerHeight + 2
40
+
41
+ return {
42
+ width,
43
+ height,
44
+ labelArea: {
45
+ x: 1 + options.padding,
46
+ y: 1 + options.padding,
47
+ width: maxLineWidth,
48
+ height: lineCount,
49
+ },
50
+ // Grid layout: [border=1, content, border=1]
51
+ gridColumns: [1, innerWidth, 1],
52
+ gridRows: [1, innerHeight, 1],
53
+ }
54
+ }
55
+
56
+ // ============================================================================
57
+ // Shared box rendering
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Render a box with custom corner characters.
62
+ * This is the core rendering function used by all rectangular shapes.
63
+ *
64
+ * @param label - Text to display in the box
65
+ * @param dimensions - Pre-calculated dimensions
66
+ * @param corners - Corner characters (tl, tr, bl, br)
67
+ * @param useAscii - Whether to use ASCII or Unicode for lines
68
+ */
69
+ export function renderBox(
70
+ label: string,
71
+ dimensions: ShapeDimensions,
72
+ corners: CornerChars,
73
+ useAscii: boolean
74
+ ): Canvas {
75
+ const { width, height } = dimensions
76
+ const canvas = mkCanvas(width - 1, height - 1)
77
+
78
+ const from = { x: 0, y: 0 }
79
+ const to = { x: width - 1, y: height - 1 }
80
+
81
+ // Line characters
82
+ const hLine = useAscii ? '-' : '─'
83
+ const vLine = useAscii ? '|' : '│'
84
+
85
+ // Draw horizontal lines (top and bottom)
86
+ for (let x = from.x + 1; x < to.x; x++) {
87
+ canvas[x]![from.y] = hLine
88
+ canvas[x]![to.y] = hLine
89
+ }
90
+
91
+ // Draw vertical lines (left and right)
92
+ for (let y = from.y + 1; y < to.y; y++) {
93
+ canvas[from.x]![y] = vLine
94
+ canvas[to.x]![y] = vLine
95
+ }
96
+
97
+ // Draw corners
98
+ canvas[from.x]![from.y] = corners.tl
99
+ canvas[to.x]![from.y] = corners.tr
100
+ canvas[from.x]![to.y] = corners.bl
101
+ canvas[to.x]![to.y] = corners.br
102
+
103
+ // Center the multi-line label
104
+ const lines = splitLines(label)
105
+ const w = width - 1 // Match original grid-based width calculation
106
+ const h = height - 1
107
+ const centerY = Math.floor(h / 2)
108
+ const startY = centerY - Math.floor((lines.length - 1) / 2)
109
+
110
+ for (let i = 0; i < lines.length; i++) {
111
+ const line = lines[i]!
112
+ const cells = toCells(line)
113
+ const textX = Math.floor(w / 2) - Math.ceil(cells.length / 2) + 1
114
+ for (let j = 0; j < cells.length; j++) {
115
+ const x = textX + j
116
+ const y = startY + i
117
+ if (x >= 0 && x < canvas.length && y >= 0 && y < canvas[0]!.length) {
118
+ canvas[x]![y] = cells[j]!
119
+ }
120
+ }
121
+ }
122
+
123
+ return canvas
124
+ }
125
+
126
+ // ============================================================================
127
+ // Shared attachment point calculation
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Calculate edge attachment point for rectangular shapes.
132
+ * All box-based shapes use the same attachment logic.
133
+ */
134
+ export function getBoxAttachmentPoint(
135
+ dir: Direction,
136
+ dimensions: ShapeDimensions,
137
+ baseCoord: DrawingCoord
138
+ ): DrawingCoord {
139
+ const { width, height } = dimensions
140
+ const centerX = baseCoord.x + Math.floor(width / 2)
141
+ const centerY = baseCoord.y + Math.floor(height / 2)
142
+
143
+ if (dirEquals(dir, Up)) return { x: centerX, y: baseCoord.y }
144
+ if (dirEquals(dir, Down)) return { x: centerX, y: baseCoord.y + height - 1 }
145
+ if (dirEquals(dir, Left)) return { x: baseCoord.x, y: centerY }
146
+ if (dirEquals(dir, Right)) return { x: baseCoord.x + width - 1, y: centerY }
147
+ if (dirEquals(dir, UpperLeft)) return { x: baseCoord.x, y: baseCoord.y }
148
+ if (dirEquals(dir, UpperRight)) return { x: baseCoord.x + width - 1, y: baseCoord.y }
149
+ if (dirEquals(dir, LowerLeft)) return { x: baseCoord.x, y: baseCoord.y + height - 1 }
150
+ if (dirEquals(dir, LowerRight)) return { x: baseCoord.x + width - 1, y: baseCoord.y + height - 1 }
151
+ // Middle
152
+ return { x: centerX, y: centerY }
153
+ }
154
+
155
+ // ============================================================================
156
+ // Rectangle renderer
157
+ // ============================================================================
158
+
159
+ /**
160
+ * Rectangle shape renderer — the default box shape.
161
+ * Renders as:
162
+ * ┌─────────┐
163
+ * │ Label │
164
+ * └─────────┘
165
+ */
166
+ export const rectangleRenderer: ShapeRenderer = {
167
+ getDimensions: getBoxDimensions,
168
+
169
+ render(label: string, dimensions: ShapeDimensions, options: ShapeRenderOptions): Canvas {
170
+ const corners = getCorners('rectangle', options.useAscii)
171
+ return renderBox(label, dimensions, corners, options.useAscii)
172
+ },
173
+
174
+ getAttachmentPoint: getBoxAttachmentPoint,
175
+ }
@@ -0,0 +1,27 @@
1
+ // ============================================================================
2
+ // Rounded rectangle shape renderer — uses rounded corner decorators
3
+ // ============================================================================
4
+
5
+ import type { ShapeRenderer } from './types'
6
+ import { getBoxDimensions, renderBox, getBoxAttachmentPoint } from './rectangle'
7
+ import { getCorners } from './corners'
8
+
9
+ /**
10
+ * Rounded rectangle shape renderer.
11
+ * Uses rounded corner markers (╭╮╰╯) to indicate soft edges.
12
+ *
13
+ * Renders as:
14
+ * ╭─────────╮
15
+ * │ Label │
16
+ * ╰─────────╯
17
+ */
18
+ export const roundedRenderer: ShapeRenderer = {
19
+ getDimensions: getBoxDimensions,
20
+
21
+ render(label, dimensions, options) {
22
+ const corners = getCorners('rounded', options.useAscii)
23
+ return renderBox(label, dimensions, corners, options.useAscii)
24
+ },
25
+
26
+ getAttachmentPoint: getBoxAttachmentPoint,
27
+ }
@@ -0,0 +1,296 @@
1
+ // ============================================================================
2
+ // Special shape renderers — subroutine, doublecircle, cylinder, etc.
3
+ // ============================================================================
4
+ //
5
+ // Some shapes have unique internal structure (subroutine, cylinder) and keep
6
+ // custom rendering. Others use the corner decorator pattern for simplicity.
7
+
8
+ import type { Canvas, DrawingCoord, Direction } from '../types'
9
+ import { Up, Down, Left, Right } from '../types'
10
+ import { mkCanvas } from '../canvas'
11
+ import { splitLines } from '../multiline-utils'
12
+ import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types'
13
+ import { dirEquals } from '../edge-routing'
14
+ import { getBoxDimensions, renderBox, getBoxAttachmentPoint } from './rectangle'
15
+ import { getCorners } from './corners'
16
+ import { displayWidth, toCells } from '../../text-metrics'
17
+
18
+ // ============================================================================
19
+ // Subroutine — keeps custom double-border rendering
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Subroutine shape renderer — double-bordered rectangle.
24
+ * Renders as:
25
+ * ┌┬─────────┬┐
26
+ * ││ Label ││
27
+ * └┴─────────┴┘
28
+ */
29
+ export const subroutineRenderer: ShapeRenderer = {
30
+ getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensions {
31
+ const lines = splitLines(label)
32
+ const maxLineWidth = Math.max(...lines.map(l => displayWidth(l)), 0)
33
+ const lineCount = lines.length
34
+
35
+ const innerWidth = 2 * options.padding + maxLineWidth
36
+ const width = innerWidth + 4 // Double borders on each side
37
+ const innerHeight = lineCount + 2 * options.padding
38
+ const height = innerHeight + 2
39
+
40
+ return {
41
+ width,
42
+ height,
43
+ labelArea: {
44
+ x: 2 + options.padding,
45
+ y: 1 + options.padding,
46
+ width: maxLineWidth,
47
+ height: lineCount,
48
+ },
49
+ gridColumns: [2, innerWidth, 2],
50
+ gridRows: [1, innerHeight, 1],
51
+ }
52
+ },
53
+
54
+ render(label: string, dimensions: ShapeDimensions, options: ShapeRenderOptions): Canvas {
55
+ const { width, height } = dimensions
56
+ const canvas = mkCanvas(width - 1, height - 1)
57
+
58
+ const hChar = options.useAscii ? '-' : '─'
59
+ const vChar = options.useAscii ? '|' : '│'
60
+
61
+ // Top border
62
+ canvas[0]![0] = options.useAscii ? '+' : '┌'
63
+ canvas[1]![0] = options.useAscii ? '+' : '┬'
64
+ for (let x = 2; x < width - 2; x++) canvas[x]![0] = hChar
65
+ canvas[width - 2]![0] = options.useAscii ? '+' : '┬'
66
+ canvas[width - 1]![0] = options.useAscii ? '+' : '┐'
67
+
68
+ // Sides with double border
69
+ for (let y = 1; y < height - 1; y++) {
70
+ canvas[0]![y] = vChar
71
+ canvas[1]![y] = vChar
72
+ canvas[width - 2]![y] = vChar
73
+ canvas[width - 1]![y] = vChar
74
+ }
75
+
76
+ // Bottom border
77
+ canvas[0]![height - 1] = options.useAscii ? '+' : '└'
78
+ canvas[1]![height - 1] = options.useAscii ? '+' : '┴'
79
+ for (let x = 2; x < width - 2; x++) canvas[x]![height - 1] = hChar
80
+ canvas[width - 2]![height - 1] = options.useAscii ? '+' : '┴'
81
+ canvas[width - 1]![height - 1] = options.useAscii ? '+' : '┘'
82
+
83
+ // Center the label
84
+ const lines = splitLines(label)
85
+ const centerY = Math.floor(height / 2)
86
+ const startY = centerY - Math.floor((lines.length - 1) / 2)
87
+
88
+ for (let i = 0; i < lines.length; i++) {
89
+ const line = lines[i]!
90
+ const cells = toCells(line)
91
+ const textX = Math.floor(width / 2) - Math.floor(cells.length / 2)
92
+ for (let j = 0; j < cells.length; j++) {
93
+ const x = textX + j
94
+ const y = startY + i
95
+ if (x > 1 && x < width - 2 && y > 0 && y < height - 1) {
96
+ canvas[x]![y] = cells[j]!
97
+ }
98
+ }
99
+ }
100
+
101
+ return canvas
102
+ },
103
+
104
+ getAttachmentPoint: getBoxAttachmentPoint,
105
+ }
106
+
107
+ // ============================================================================
108
+ // Double circle — uses corner decorators
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Double circle shape renderer.
113
+ * Uses double circle markers (◎) at corners.
114
+ *
115
+ * Renders as:
116
+ * ◎─────────◎
117
+ * │ Label │
118
+ * ◎─────────◎
119
+ */
120
+ export const doublecircleRenderer: ShapeRenderer = {
121
+ getDimensions: getBoxDimensions,
122
+
123
+ render(label, dimensions, options) {
124
+ const corners = getCorners('doublecircle', options.useAscii)
125
+ return renderBox(label, dimensions, corners, options.useAscii)
126
+ },
127
+
128
+ getAttachmentPoint: getBoxAttachmentPoint,
129
+ }
130
+
131
+ // ============================================================================
132
+ // Cylinder — keeps custom rendering for database appearance
133
+ // ============================================================================
134
+
135
+ /**
136
+ * Cylinder shape renderer — database symbol.
137
+ * Renders as:
138
+ * ╭─────╮
139
+ * │─────│
140
+ * │ DB │
141
+ * │─────│
142
+ * ╰─────╯
143
+ */
144
+ export const cylinderRenderer: ShapeRenderer = {
145
+ getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensions {
146
+ const lines = splitLines(label)
147
+ const maxLineWidth = Math.max(...lines.map(l => displayWidth(l)), 0)
148
+ const lineCount = lines.length
149
+
150
+ const innerWidth = 2 * options.padding + maxLineWidth
151
+ const width = innerWidth + 2
152
+ const innerHeight = lineCount + 2 * options.padding + 2 // Extra for curved top/bottom
153
+ const height = innerHeight + 2
154
+
155
+ return {
156
+ width,
157
+ height,
158
+ labelArea: {
159
+ x: 1 + options.padding,
160
+ y: 2 + options.padding,
161
+ width: maxLineWidth,
162
+ height: lineCount,
163
+ },
164
+ gridColumns: [1, innerWidth, 1],
165
+ gridRows: [2, innerHeight - 2, 2],
166
+ }
167
+ },
168
+
169
+ render(label: string, dimensions: ShapeDimensions, options: ShapeRenderOptions): Canvas {
170
+ const { width, height } = dimensions
171
+ const canvas = mkCanvas(width - 1, height - 1)
172
+
173
+ const hChar = options.useAscii ? '-' : '─'
174
+ const vChar = options.useAscii ? '|' : '│'
175
+
176
+ // Top ellipse
177
+ canvas[0]![0] = options.useAscii ? '.' : '╭'
178
+ for (let x = 1; x < width - 1; x++) canvas[x]![0] = hChar
179
+ canvas[width - 1]![0] = options.useAscii ? '.' : '╮'
180
+
181
+ // Second row - bottom of top ellipse
182
+ canvas[0]![1] = vChar
183
+ for (let x = 1; x < width - 1; x++) canvas[x]![1] = hChar
184
+ canvas[width - 1]![1] = vChar
185
+
186
+ // Middle section
187
+ for (let y = 2; y < height - 2; y++) {
188
+ canvas[0]![y] = vChar
189
+ canvas[width - 1]![y] = vChar
190
+ }
191
+
192
+ // Second to last row - top of bottom ellipse
193
+ canvas[0]![height - 2] = vChar
194
+ for (let x = 1; x < width - 1; x++) canvas[x]![height - 2] = hChar
195
+ canvas[width - 1]![height - 2] = vChar
196
+
197
+ // Bottom ellipse
198
+ canvas[0]![height - 1] = options.useAscii ? '\'' : '╰'
199
+ for (let x = 1; x < width - 1; x++) canvas[x]![height - 1] = hChar
200
+ canvas[width - 1]![height - 1] = options.useAscii ? '\'' : '╯'
201
+
202
+ // Center the label
203
+ const lines = splitLines(label)
204
+ const centerY = Math.floor(height / 2)
205
+ const startY = centerY - Math.floor((lines.length - 1) / 2)
206
+
207
+ for (let i = 0; i < lines.length; i++) {
208
+ const line = lines[i]!
209
+ const cells = toCells(line)
210
+ const textX = Math.floor(width / 2) - Math.floor(cells.length / 2)
211
+ for (let j = 0; j < cells.length; j++) {
212
+ const x = textX + j
213
+ const y = startY + i
214
+ if (x > 0 && x < width - 1 && y > 1 && y < height - 2) {
215
+ canvas[x]![y] = cells[j]!
216
+ }
217
+ }
218
+ }
219
+
220
+ return canvas
221
+ },
222
+
223
+ getAttachmentPoint: getBoxAttachmentPoint,
224
+ }
225
+
226
+ // ============================================================================
227
+ // Asymmetric (flag) — uses corner decorators
228
+ // ============================================================================
229
+
230
+ /**
231
+ * Asymmetric (flag/banner) shape renderer.
232
+ * Uses arrow markers (▷) on left corners.
233
+ *
234
+ * Renders as:
235
+ * ▷─────────┐
236
+ * │ Label │
237
+ * ▷─────────┘
238
+ */
239
+ export const asymmetricRenderer: ShapeRenderer = {
240
+ getDimensions: getBoxDimensions,
241
+
242
+ render(label, dimensions, options) {
243
+ const corners = getCorners('asymmetric', options.useAscii)
244
+ return renderBox(label, dimensions, corners, options.useAscii)
245
+ },
246
+
247
+ getAttachmentPoint: getBoxAttachmentPoint,
248
+ }
249
+
250
+ // ============================================================================
251
+ // Trapezoid — uses corner decorators instead of diagonal sides
252
+ // ============================================================================
253
+
254
+ /**
255
+ * Trapezoid shape renderer — wider at bottom.
256
+ * Uses slope markers (◸◹) on top corners.
257
+ *
258
+ * Renders as:
259
+ * ◸─────────◹
260
+ * │ Label │
261
+ * └─────────┘
262
+ */
263
+ export const trapezoidRenderer: ShapeRenderer = {
264
+ getDimensions: getBoxDimensions,
265
+
266
+ render(label, dimensions, options) {
267
+ const corners = getCorners('trapezoid', options.useAscii)
268
+ return renderBox(label, dimensions, corners, options.useAscii)
269
+ },
270
+
271
+ getAttachmentPoint: getBoxAttachmentPoint,
272
+ }
273
+
274
+ // ============================================================================
275
+ // Trapezoid-alt — uses corner decorators instead of diagonal sides
276
+ // ============================================================================
277
+
278
+ /**
279
+ * Trapezoid-alt shape renderer — wider at top.
280
+ * Uses slope markers (◺◿) on bottom corners.
281
+ *
282
+ * Renders as:
283
+ * ┌─────────┐
284
+ * │ Label │
285
+ * ◺─────────◿
286
+ */
287
+ export const trapezoidAltRenderer: ShapeRenderer = {
288
+ getDimensions: getBoxDimensions,
289
+
290
+ render(label, dimensions, options) {
291
+ const corners = getCorners('trapezoid-alt', options.useAscii)
292
+ return renderBox(label, dimensions, corners, options.useAscii)
293
+ },
294
+
295
+ getAttachmentPoint: getBoxAttachmentPoint,
296
+ }