@diagrammo/dgmo 0.8.21 → 0.8.22
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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +143 -93
- package/dist/editor.cjs +17 -3
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +17 -3
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +12 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +12 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +19997 -14886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +331 -8
- package/dist/index.d.ts +331 -8
- package/dist/index.js +19984 -14889
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +116 -6
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +1 -1
- package/src/boxes-and-lines/layout.ts +309 -33
- package/src/boxes-and-lines/parser.ts +86 -10
- package/src/boxes-and-lines/renderer.ts +250 -91
- package/src/boxes-and-lines/types.ts +1 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/completion.ts +227 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +87 -8
- package/src/dgmo-router.ts +9 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +14 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/state-parser.ts +5 -10
- package/src/index.ts +50 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +31 -15
- package/src/mindmap/parser.ts +12 -18
- package/src/mindmap/renderer.ts +14 -13
- package/src/mindmap/text-wrap.ts +22 -12
- package/src/mindmap/types.ts +2 -2
- package/src/org/parser.ts +2 -6
- package/src/sequence/renderer.ts +144 -38
- package/src/sharing.ts +1 -0
- package/src/sitemap/layout.ts +21 -6
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1058 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/legend-layout.ts +3 -1
- package/src/utils/parsing.ts +46 -7
- package/src/utils/tag-groups.ts +46 -60
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { PaletteColors } from '../palettes';
|
|
2
|
+
import { mix } from '../palettes/color-utils';
|
|
3
|
+
import type {
|
|
4
|
+
ParsedJourneyMap,
|
|
5
|
+
JourneyMapPhase,
|
|
6
|
+
JourneyMapStep,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// Layout Types
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
export interface CurvePoint {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
score: number;
|
|
17
|
+
emotionLabel?: string;
|
|
18
|
+
stepIndex: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StepLayout {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
step: JourneyMapStep;
|
|
27
|
+
color: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PhaseLayout {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
phase: JourneyMapPhase;
|
|
36
|
+
headerColor: string;
|
|
37
|
+
stepLayouts: StepLayout[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface JourneyMapLayout {
|
|
41
|
+
phases: PhaseLayout[];
|
|
42
|
+
flatStepLayouts: StepLayout[];
|
|
43
|
+
curvePoints: CurvePoint[];
|
|
44
|
+
totalWidth: number;
|
|
45
|
+
totalHeight: number;
|
|
46
|
+
curveAreaTop: number;
|
|
47
|
+
curveAreaBottom: number;
|
|
48
|
+
cardAreaTop: number;
|
|
49
|
+
personaHeight: number;
|
|
50
|
+
titleHeight: number;
|
|
51
|
+
/** Whether any step has thought annotations */
|
|
52
|
+
hasThoughts: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================
|
|
56
|
+
// Constants
|
|
57
|
+
// ============================================================
|
|
58
|
+
|
|
59
|
+
const PADDING = 24;
|
|
60
|
+
const TITLE_HEIGHT = 36;
|
|
61
|
+
const PERSONA_HEIGHT = 48;
|
|
62
|
+
const CURVE_AREA_HEIGHT = 260;
|
|
63
|
+
const CARD_GAP = 8;
|
|
64
|
+
const STEP_CARD_WIDTH = 190;
|
|
65
|
+
const CARD_HEADER_HEIGHT = 24;
|
|
66
|
+
const CARD_META_LINE_HEIGHT = 14;
|
|
67
|
+
const PHASE_HEADER_HEIGHT = 36;
|
|
68
|
+
const CARD_PADDING_X = 10;
|
|
69
|
+
const CARD_PADDING_Y = 6;
|
|
70
|
+
const ANNO_ICON_SIZE = 10;
|
|
71
|
+
const ANNO_ICON_GAP = 4;
|
|
72
|
+
export const TAG_STRIP_HEIGHT = 18;
|
|
73
|
+
const PHASE_GAP = 16;
|
|
74
|
+
const COLUMN_PADDING = 12;
|
|
75
|
+
const FACE_ICON_SIZE = 20;
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// Score-to-color
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
export function scoreToColor(score: number, palette: PaletteColors): string {
|
|
82
|
+
// 5=green, 1=red — interpolate
|
|
83
|
+
const t = ((5 - score) / 4) * 100;
|
|
84
|
+
return mix(palette.colors.red, palette.colors.green, t);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Layout Engine
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
export const COLLAPSED_PHASE_WIDTH = 60;
|
|
92
|
+
|
|
93
|
+
export function layoutJourneyMap(
|
|
94
|
+
parsed: ParsedJourneyMap,
|
|
95
|
+
palette: PaletteColors,
|
|
96
|
+
options?: {
|
|
97
|
+
exportDims?: { width: number; height: number };
|
|
98
|
+
collapsedPhases?: Set<string>;
|
|
99
|
+
}
|
|
100
|
+
): JourneyMapLayout {
|
|
101
|
+
const hasTitle = !!parsed.title;
|
|
102
|
+
const hasPersona = !!parsed.persona;
|
|
103
|
+
const hasPhases = parsed.phases.length > 0;
|
|
104
|
+
|
|
105
|
+
const titleHeight = hasTitle ? TITLE_HEIGHT : 0;
|
|
106
|
+
const personaHeight = hasPersona ? PERSONA_HEIGHT : 0;
|
|
107
|
+
|
|
108
|
+
// Thought bubbles render as overlays on hover — no reserved vertical space
|
|
109
|
+
const allStepsForThoughts = hasPhases
|
|
110
|
+
? parsed.phases.flatMap((p) => p.steps)
|
|
111
|
+
: parsed.steps;
|
|
112
|
+
const hasThoughts = allStepsForThoughts.some((s) =>
|
|
113
|
+
s.annotations.some((a) => a.type === 'thought')
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const curveAreaTop = PADDING + titleHeight + personaHeight;
|
|
117
|
+
const curveAreaBottom = curveAreaTop + CURVE_AREA_HEIGHT;
|
|
118
|
+
const cardAreaTop = curveAreaBottom + PADDING;
|
|
119
|
+
|
|
120
|
+
const allSteps = hasPhases
|
|
121
|
+
? parsed.phases.flatMap((p) => p.steps)
|
|
122
|
+
: parsed.steps;
|
|
123
|
+
|
|
124
|
+
// Compute step card heights based on content (matches kanban card sizing)
|
|
125
|
+
const annoIconIndent = ANNO_ICON_SIZE + ANNO_ICON_GAP;
|
|
126
|
+
const annoTextW = STEP_CARD_WIDTH - CARD_PADDING_X * 2 - annoIconIndent;
|
|
127
|
+
const descTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
128
|
+
const charWidth = 4.8; // average char width at FONT_SIZE_META (10px)
|
|
129
|
+
|
|
130
|
+
const titleTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
131
|
+
const titleCharWidth = 6.5; // average char width at FONT_SIZE_STEP (12px)
|
|
132
|
+
const TITLE_LINE_HEIGHT = 16;
|
|
133
|
+
|
|
134
|
+
const stepHeights = allSteps.map((step) => {
|
|
135
|
+
const titleLines = wrapLineCount(
|
|
136
|
+
step.title,
|
|
137
|
+
titleTextWidth,
|
|
138
|
+
titleCharWidth
|
|
139
|
+
);
|
|
140
|
+
let h = CARD_PADDING_Y + titleLines * TITLE_LINE_HEIGHT + CARD_PADDING_Y;
|
|
141
|
+
const cardAnnos = step.annotations;
|
|
142
|
+
let contentLines = 0;
|
|
143
|
+
// Description may wrap
|
|
144
|
+
if (step.description) {
|
|
145
|
+
contentLines += wrapLineCount(step.description, descTextWidth, charWidth);
|
|
146
|
+
}
|
|
147
|
+
// Annotations: all lines indented past icon
|
|
148
|
+
for (const anno of cardAnnos) {
|
|
149
|
+
contentLines += wrapLineCount(anno.text, annoTextW, charWidth);
|
|
150
|
+
}
|
|
151
|
+
if (contentLines > 0) {
|
|
152
|
+
h += contentLines * CARD_META_LINE_HEIGHT + 4; // 4px bottom padding
|
|
153
|
+
}
|
|
154
|
+
return h;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const minCardHeight = CARD_HEADER_HEIGHT + CARD_META_LINE_HEIGHT;
|
|
158
|
+
const maxCardHeight = Math.max(minCardHeight, ...stepHeights);
|
|
159
|
+
|
|
160
|
+
// Check if any step has tags — if so, reserve space for the tag strip above cards
|
|
161
|
+
const hasTags = allSteps.some((s) => Object.keys(s.tags).length > 0);
|
|
162
|
+
const tagStripOffset = hasTags ? TAG_STRIP_HEIGHT + 6 : 0;
|
|
163
|
+
|
|
164
|
+
// Layout phases or flat steps
|
|
165
|
+
const phaseLayouts: PhaseLayout[] = [];
|
|
166
|
+
const flatStepLayouts: StepLayout[] = [];
|
|
167
|
+
const curvePoints: CurvePoint[] = [];
|
|
168
|
+
|
|
169
|
+
let globalStepIndex = 0;
|
|
170
|
+
|
|
171
|
+
const collapsed = options?.collapsedPhases ?? new Set<string>();
|
|
172
|
+
|
|
173
|
+
if (hasPhases) {
|
|
174
|
+
let phaseX = PADDING;
|
|
175
|
+
|
|
176
|
+
for (const phase of parsed.phases) {
|
|
177
|
+
const isCollapsed = collapsed.has(phase.name);
|
|
178
|
+
const stepCount = Math.max(phase.steps.length, 1);
|
|
179
|
+
const phaseWidth = isCollapsed
|
|
180
|
+
? STEP_CARD_WIDTH + COLUMN_PADDING * 2
|
|
181
|
+
: stepCount * STEP_CARD_WIDTH +
|
|
182
|
+
(stepCount - 1) * CARD_GAP +
|
|
183
|
+
COLUMN_PADDING * 2;
|
|
184
|
+
|
|
185
|
+
const stepLayouts: StepLayout[] = [];
|
|
186
|
+
|
|
187
|
+
if (!isCollapsed) {
|
|
188
|
+
let stepX = phaseX + COLUMN_PADDING;
|
|
189
|
+
|
|
190
|
+
for (let si = 0; si < phase.steps.length; si++) {
|
|
191
|
+
const step = phase.steps[si];
|
|
192
|
+
const color =
|
|
193
|
+
step.score !== undefined
|
|
194
|
+
? scoreToColor(step.score, palette)
|
|
195
|
+
: palette.surface;
|
|
196
|
+
|
|
197
|
+
const sl: StepLayout = {
|
|
198
|
+
x: stepX,
|
|
199
|
+
y: cardAreaTop + PHASE_HEADER_HEIGHT + CARD_GAP + tagStripOffset,
|
|
200
|
+
width: STEP_CARD_WIDTH,
|
|
201
|
+
height: maxCardHeight,
|
|
202
|
+
step,
|
|
203
|
+
color,
|
|
204
|
+
};
|
|
205
|
+
stepLayouts.push(sl);
|
|
206
|
+
|
|
207
|
+
// Curve point
|
|
208
|
+
if (step.score !== undefined) {
|
|
209
|
+
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
210
|
+
const curveY =
|
|
211
|
+
curveAreaBottom -
|
|
212
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
213
|
+
10;
|
|
214
|
+
curvePoints.push({
|
|
215
|
+
x: curveX,
|
|
216
|
+
y: curveY,
|
|
217
|
+
score: step.score,
|
|
218
|
+
emotionLabel: step.emotionLabel,
|
|
219
|
+
stepIndex: globalStepIndex,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
stepX += STEP_CARD_WIDTH + CARD_GAP;
|
|
224
|
+
globalStepIndex++;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Collapsed: spread curve points across the compressed column width
|
|
228
|
+
const stepCount = phase.steps.length;
|
|
229
|
+
const padX = COLUMN_PADDING + FACE_ICON_SIZE;
|
|
230
|
+
const availW = phaseWidth - padX * 2;
|
|
231
|
+
for (let si = 0; si < stepCount; si++) {
|
|
232
|
+
const step = phase.steps[si];
|
|
233
|
+
if (step.score !== undefined) {
|
|
234
|
+
const curveX =
|
|
235
|
+
stepCount === 1
|
|
236
|
+
? phaseX + phaseWidth / 2
|
|
237
|
+
: phaseX + padX + (si / (stepCount - 1)) * availW;
|
|
238
|
+
const curveY =
|
|
239
|
+
curveAreaBottom -
|
|
240
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
241
|
+
10;
|
|
242
|
+
curvePoints.push({
|
|
243
|
+
x: curveX,
|
|
244
|
+
y: curveY,
|
|
245
|
+
score: step.score,
|
|
246
|
+
emotionLabel: step.emotionLabel,
|
|
247
|
+
stepIndex: globalStepIndex,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
globalStepIndex++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Phase header color from average score
|
|
255
|
+
const scoredSteps = phase.steps.filter((s) => s.score !== undefined);
|
|
256
|
+
const avgScore =
|
|
257
|
+
scoredSteps.length > 0
|
|
258
|
+
? scoredSteps.reduce((sum, s) => sum + s.score!, 0) /
|
|
259
|
+
scoredSteps.length
|
|
260
|
+
: 3;
|
|
261
|
+
const headerColor = mix(scoreToColor(avgScore, palette), palette.bg, 25);
|
|
262
|
+
|
|
263
|
+
const COLLAPSED_CARD_H = 26;
|
|
264
|
+
const COLLAPSED_GAP = 6;
|
|
265
|
+
const phaseHeight = isCollapsed
|
|
266
|
+
? PHASE_HEADER_HEIGHT +
|
|
267
|
+
CARD_GAP +
|
|
268
|
+
phase.steps.length * (COLLAPSED_CARD_H + COLLAPSED_GAP) +
|
|
269
|
+
CARD_GAP
|
|
270
|
+
: PHASE_HEADER_HEIGHT +
|
|
271
|
+
CARD_GAP +
|
|
272
|
+
tagStripOffset +
|
|
273
|
+
maxCardHeight +
|
|
274
|
+
CARD_GAP;
|
|
275
|
+
|
|
276
|
+
phaseLayouts.push({
|
|
277
|
+
x: phaseX,
|
|
278
|
+
y: cardAreaTop,
|
|
279
|
+
width: phaseWidth,
|
|
280
|
+
height: phaseHeight,
|
|
281
|
+
phase,
|
|
282
|
+
headerColor,
|
|
283
|
+
stepLayouts,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
phaseX += phaseWidth + PHASE_GAP;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Flat mode
|
|
290
|
+
let stepX = PADDING;
|
|
291
|
+
|
|
292
|
+
for (let si = 0; si < parsed.steps.length; si++) {
|
|
293
|
+
const step = parsed.steps[si];
|
|
294
|
+
const color =
|
|
295
|
+
step.score !== undefined
|
|
296
|
+
? scoreToColor(step.score, palette)
|
|
297
|
+
: palette.surface;
|
|
298
|
+
|
|
299
|
+
flatStepLayouts.push({
|
|
300
|
+
x: stepX,
|
|
301
|
+
y: cardAreaTop + CARD_GAP + tagStripOffset,
|
|
302
|
+
width: STEP_CARD_WIDTH,
|
|
303
|
+
height: maxCardHeight,
|
|
304
|
+
step,
|
|
305
|
+
color,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (step.score !== undefined) {
|
|
309
|
+
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
310
|
+
const curveY =
|
|
311
|
+
curveAreaBottom -
|
|
312
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 20) -
|
|
313
|
+
10;
|
|
314
|
+
curvePoints.push({
|
|
315
|
+
x: curveX,
|
|
316
|
+
y: curveY,
|
|
317
|
+
score: step.score,
|
|
318
|
+
emotionLabel: step.emotionLabel,
|
|
319
|
+
stepIndex: si,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
stepX += STEP_CARD_WIDTH + CARD_GAP;
|
|
324
|
+
globalStepIndex++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Compute total dimensions
|
|
329
|
+
const rightEdge = hasPhases
|
|
330
|
+
? phaseLayouts.length > 0
|
|
331
|
+
? phaseLayouts[phaseLayouts.length - 1].x +
|
|
332
|
+
phaseLayouts[phaseLayouts.length - 1].width +
|
|
333
|
+
PADDING
|
|
334
|
+
: PADDING * 2
|
|
335
|
+
: flatStepLayouts.length > 0
|
|
336
|
+
? flatStepLayouts[flatStepLayouts.length - 1].x +
|
|
337
|
+
STEP_CARD_WIDTH +
|
|
338
|
+
PADDING
|
|
339
|
+
: PADDING * 2;
|
|
340
|
+
|
|
341
|
+
const bottomEdge = hasPhases
|
|
342
|
+
? phaseLayouts.length > 0
|
|
343
|
+
? phaseLayouts[0].y + phaseLayouts[0].height + PADDING + 40
|
|
344
|
+
: cardAreaTop + PADDING
|
|
345
|
+
: cardAreaTop + CARD_GAP + tagStripOffset + maxCardHeight + PADDING + 40;
|
|
346
|
+
|
|
347
|
+
// Add space for score legend at bottom
|
|
348
|
+
const totalWidth = Math.max(rightEdge, 400);
|
|
349
|
+
const totalHeight = bottomEdge;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
phases: phaseLayouts,
|
|
353
|
+
flatStepLayouts,
|
|
354
|
+
curvePoints,
|
|
355
|
+
totalWidth,
|
|
356
|
+
totalHeight,
|
|
357
|
+
curveAreaTop,
|
|
358
|
+
curveAreaBottom,
|
|
359
|
+
cardAreaTop,
|
|
360
|
+
personaHeight,
|
|
361
|
+
titleHeight,
|
|
362
|
+
hasThoughts,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Count how many visual lines a text string will occupy when wrapped. */
|
|
367
|
+
function wrapLineCount(
|
|
368
|
+
text: string,
|
|
369
|
+
maxWidth: number,
|
|
370
|
+
charWidth: number
|
|
371
|
+
): number {
|
|
372
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
|
|
373
|
+
const words = text.split(/\s+/);
|
|
374
|
+
let lines = 1;
|
|
375
|
+
let currentLen = 0;
|
|
376
|
+
for (const word of words) {
|
|
377
|
+
const needed = currentLen > 0 ? word.length + 1 : word.length;
|
|
378
|
+
if (currentLen + needed > maxChars && currentLen > 0) {
|
|
379
|
+
lines++;
|
|
380
|
+
currentLen = word.length;
|
|
381
|
+
} else {
|
|
382
|
+
currentLen += needed;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return lines;
|
|
386
|
+
}
|