@transitrix/diagrams 1.8.1 → 1.8.3
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/dist/__tests__/node-size-presets.test.d.ts +1 -0
- package/dist/__tests__/node-size-presets.test.js +30 -0
- package/dist/activities/layout.js +1 -1
- package/dist/blocks/__tests__/layout.test.js +2 -2
- package/dist/blocks/layout.js +4 -3
- package/dist/capability-map/dsm-layout.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +1 -1
- package/dist/capability-map/layout-tree.js +1 -1
- package/dist/fgca/preview-layout.d.ts +2 -2
- package/dist/fgca/preview-layout.js +3 -2
- package/dist/node-size-presets.d.ts +17 -4
- package/dist/node-size-presets.js +86 -72
- package/dist/webview/__tests__/entity-text-layout.test.js +115 -3
- package/dist/webview/__tests__/render-blocks.test.js +9 -6
- package/dist/webview/__tests__/render-goals.test.js +41 -0
- package/dist/webview/entity-text-layout.d.ts +31 -3
- package/dist/webview/entity-text-layout.js +123 -57
- package/package.json +1 -1
- package/src/__tests__/node-size-presets.test.ts +50 -0
- package/src/activities/layout.ts +1 -1
- package/src/blocks/__tests__/layout.test.ts +2 -2
- package/src/blocks/layout.ts +4 -3
- package/src/capability-map/dsm-layout.ts +1 -1
- package/src/capability-map/layout-tree.ts +1 -1
- package/src/fgca/preview-layout.ts +3 -2
- package/src/node-size-presets.ts +95 -72
- package/src/webview/__tests__/entity-text-layout.test.ts +132 -2
- package/src/webview/__tests__/render-blocks.test.ts +11 -6
- package/src/webview/__tests__/render-goals.test.ts +42 -0
- package/src/webview/entity-text-layout.ts +140 -54
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const TEXT_MARGIN_X = 8;
|
|
10
|
+
/**
|
|
11
|
+
* Minimum reserved vertical padding (px) between the box's top/bottom edge and
|
|
12
|
+
* the nearest line's outer extent. Always enforced — see
|
|
13
|
+
* {@link layoutCenteredEntityText}'s adaptive line-count reduction, which
|
|
14
|
+
* trades name-line count for this padding rather than letting text touch or
|
|
15
|
+
* cross the box border.
|
|
16
|
+
*/
|
|
17
|
+
export const TEXT_MARGIN_Y = 4;
|
|
10
18
|
/** Approximate character width (px) at text-primary size (12px/600). */
|
|
11
19
|
export const CHAR_W_PRIMARY = 7;
|
|
12
20
|
/** Approximate character width (px) at text-id size (10px/600). */
|
|
@@ -19,10 +27,15 @@ export const LINE_H_PRIMARY = 14;
|
|
|
19
27
|
export const LINE_H_SECONDARY = 14;
|
|
20
28
|
/** Vertical advance between id line centres (text-id). */
|
|
21
29
|
export const LINE_H_ID = 12;
|
|
22
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Gap between the name block and the id block in leaf-style layouts — used as
|
|
32
|
+
* a direct centre-to-centre distance there, so keep it >= ~11px (half of
|
|
33
|
+
* text-primary's 12px font + half of text-id's 10px font) to avoid the
|
|
34
|
+
* overlap regressed in #419. Do not tighten without re-checking that test.
|
|
35
|
+
*/
|
|
23
36
|
export const NAME_ID_GAP = 14;
|
|
24
37
|
/** Gap between stacked row groups (name → type → id). */
|
|
25
|
-
export const ROW_GROUP_GAP =
|
|
38
|
+
export const ROW_GROUP_GAP = 4;
|
|
26
39
|
|
|
27
40
|
export function maxCharsForInnerWidth(innerWidth: number, charWidth: number): number {
|
|
28
41
|
return Math.max(4, Math.floor(Math.max(0, innerWidth) / charWidth));
|
|
@@ -156,6 +169,7 @@ export interface LayoutCenteredEntityOptions {
|
|
|
156
169
|
nameMaxLines?: number;
|
|
157
170
|
idMaxLines?: number;
|
|
158
171
|
marginX?: number;
|
|
172
|
+
marginY?: number;
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
interface TextRowGroup {
|
|
@@ -165,69 +179,111 @@ interface TextRowGroup {
|
|
|
165
179
|
gapAfter: number;
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Half-extent (px) of rendered glyphs around their centre y — approximately
|
|
184
|
+
* font-size / 2, the same heuristic the #419 non-overlap regression test
|
|
185
|
+
* uses. Used only to keep the *outermost* line's visual extent (not just its
|
|
186
|
+
* centre) inside the box when computing edge padding; inter-line spacing
|
|
187
|
+
* between adjacent rows is governed separately by the LINE_H_ constants,
|
|
188
|
+
* NAME_ID_GAP and ROW_GROUP_GAP, which already keep centres far enough apart
|
|
189
|
+
* to avoid overlap.
|
|
190
|
+
*/
|
|
191
|
+
const HALF_EXTENT_PRIMARY = 6; // text-primary, 12px font
|
|
192
|
+
const HALF_EXTENT_ID = 5; // text-id, 10px font
|
|
193
|
+
|
|
194
|
+
/** Place `groups`' lines starting at `startY`, advancing by each line's own height and gap. */
|
|
195
|
+
function placeLines(groups: TextRowGroup[], startY: number): TextLineSpec[] {
|
|
196
|
+
const out: TextLineSpec[] = [];
|
|
197
|
+
let y = startY;
|
|
198
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
199
|
+
const g = groups[gi];
|
|
200
|
+
for (let li = 0; li < g.lines.length; li++) {
|
|
201
|
+
out.push({ cls: g.cls, text: g.lines[li], y });
|
|
202
|
+
y += g.lineHeight;
|
|
203
|
+
}
|
|
204
|
+
if (gi < groups.length - 1) y += g.gapAfter;
|
|
174
205
|
}
|
|
175
|
-
return
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Centre-to-centre distance from the first to the last placed line. */
|
|
210
|
+
function centerSpan(groups: TextRowGroup[]): number {
|
|
211
|
+
const ys = placeLines(groups, 0).map((s) => s.y);
|
|
212
|
+
return ys[ys.length - 1] - ys[0];
|
|
176
213
|
}
|
|
177
214
|
|
|
178
215
|
/**
|
|
179
216
|
* Layout centred entity text (name → optional type → id) inside a fixed box.
|
|
180
217
|
* Returns absolute SVG y-positions (line centres) for each rendered row.
|
|
218
|
+
*
|
|
219
|
+
* Guarantees, in order of priority:
|
|
220
|
+
* 1. Content never crosses the box's top/bottom edge.
|
|
221
|
+
* 2. At least `marginY` (default {@link TEXT_MARGIN_Y}) of padding is kept
|
|
222
|
+
* above the first line's glyphs and below the last line's glyphs.
|
|
223
|
+
* 3. The name gets as many lines as `nameMaxLines` allows (2 by default)
|
|
224
|
+
* when the box is tall enough; on short/compact boxes (e.g. with a type
|
|
225
|
+
* row present) it degrades to fewer name lines — trading line count,
|
|
226
|
+
* not padding — rather than letting text overflow.
|
|
181
227
|
*/
|
|
182
228
|
export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): TextLineSpec[] {
|
|
183
229
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
230
|
+
const marginY = opts.marginY ?? TEXT_MARGIN_Y;
|
|
184
231
|
const innerW = Math.max(0, opts.boxWidth - marginX * 2);
|
|
185
|
-
const cx = opts.boxX + opts.boxWidth / 2;
|
|
186
232
|
const maxCharsName = maxCharsForInnerWidth(innerW, CHAR_W_PRIMARY);
|
|
187
233
|
const maxCharsType = maxCharsForInnerWidth(innerW, CHAR_W_SECONDARY);
|
|
188
234
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
189
235
|
|
|
190
|
-
const nameLines = wrapWords(opts.name, maxCharsName, opts.nameMaxLines ?? 2);
|
|
191
236
|
const typeLine = opts.type?.trim() ? truncateLine(opts.type, maxCharsType) : undefined;
|
|
192
237
|
const idMaxLines = opts.idMaxLines ?? 1;
|
|
193
238
|
const idLines = idMaxLines <= 1 ? [truncateId(opts.id, maxCharsId)] : wrapId(opts.id, maxCharsId, idMaxLines);
|
|
194
239
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
function buildGroups(nameLines: string[]): TextRowGroup[] {
|
|
241
|
+
const groups: TextRowGroup[] = [
|
|
242
|
+
{
|
|
243
|
+
cls: 'text-primary',
|
|
244
|
+
lines: nameLines,
|
|
245
|
+
lineHeight: LINE_H_PRIMARY,
|
|
246
|
+
gapAfter: typeLine ? ROW_GROUP_GAP : NAME_ID_GAP,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
if (typeLine) {
|
|
250
|
+
groups.push({
|
|
251
|
+
cls: 'text-secondary',
|
|
252
|
+
lines: [typeLine],
|
|
253
|
+
lineHeight: LINE_H_SECONDARY,
|
|
254
|
+
gapAfter: NAME_ID_GAP,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
204
257
|
groups.push({
|
|
205
|
-
cls: 'text-
|
|
206
|
-
lines:
|
|
207
|
-
lineHeight:
|
|
208
|
-
gapAfter:
|
|
258
|
+
cls: 'text-id',
|
|
259
|
+
lines: idLines,
|
|
260
|
+
lineHeight: LINE_H_ID,
|
|
261
|
+
gapAfter: 0,
|
|
209
262
|
});
|
|
263
|
+
return groups;
|
|
210
264
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (gi < groups.length - 1) y += g.gapAfter;
|
|
265
|
+
|
|
266
|
+
// groups[0] is always the name (text-primary) and the last group is always
|
|
267
|
+
// the id (text-id), so the edge-padding half-extents below are fixed.
|
|
268
|
+
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 2);
|
|
269
|
+
const availableHeight = Math.max(0, opts.boxHeight - marginY * 2);
|
|
270
|
+
|
|
271
|
+
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
272
|
+
let groups = buildGroups(nameLines);
|
|
273
|
+
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
274
|
+
for (
|
|
275
|
+
let cap = requestedNameMaxLines - 1;
|
|
276
|
+
cap >= 1 && contentSpan > availableHeight;
|
|
277
|
+
cap--
|
|
278
|
+
) {
|
|
279
|
+
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
280
|
+
groups = buildGroups(nameLines);
|
|
281
|
+
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
229
282
|
}
|
|
230
|
-
|
|
283
|
+
|
|
284
|
+
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
285
|
+
const firstY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
286
|
+
return placeLines(groups, firstY);
|
|
231
287
|
}
|
|
232
288
|
|
|
233
289
|
/** Emit centred `<text>` elements from {@link layoutCenteredEntityText} specs. */
|
|
@@ -256,25 +312,44 @@ export interface LayoutLeafBlockTextOptions {
|
|
|
256
312
|
marginX?: number;
|
|
257
313
|
}
|
|
258
314
|
|
|
259
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Leaf nested-block layout: name (up to 3 lines) + id (up to 2 lines), centred.
|
|
317
|
+
* Degrades name line count (never below 1) when the box is too short to hold
|
|
318
|
+
* the requested max plus {@link TEXT_MARGIN_Y} padding on both edges, so text
|
|
319
|
+
* never crosses the box border.
|
|
320
|
+
*/
|
|
260
321
|
export function layoutLeafBlockText(opts: LayoutLeafBlockTextOptions): TextLineSpec[] {
|
|
261
322
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
262
323
|
const innerW = Math.max(0, opts.boxWidth - marginX * 2);
|
|
263
324
|
const maxCharsName = maxCharsForInnerWidth(innerW, CHAR_W_PRIMARY);
|
|
264
325
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
265
|
-
const nameLines = wrapWords(opts.name, maxCharsName, opts.nameMaxLines ?? 3);
|
|
266
326
|
const idLines = wrapId(opts.id, maxCharsId, 2);
|
|
327
|
+
const idCenterSpan = (idLines.length - 1) * LINE_H_ID;
|
|
328
|
+
|
|
329
|
+
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 3);
|
|
330
|
+
const availableHeight = Math.max(0, opts.boxHeight - TEXT_MARGIN_Y * 2);
|
|
331
|
+
|
|
332
|
+
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
333
|
+
let nameCenterSpan = (nameLines.length - 1) * LINE_H_PRIMARY;
|
|
334
|
+
let contentSpan = nameCenterSpan + NAME_ID_GAP + idCenterSpan + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
335
|
+
for (
|
|
336
|
+
let cap = requestedNameMaxLines - 1;
|
|
337
|
+
cap >= 1 && contentSpan > availableHeight;
|
|
338
|
+
cap--
|
|
339
|
+
) {
|
|
340
|
+
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
341
|
+
nameCenterSpan = (nameLines.length - 1) * LINE_H_PRIMARY;
|
|
342
|
+
contentSpan = nameCenterSpan + NAME_ID_GAP + idCenterSpan + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
343
|
+
}
|
|
267
344
|
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const totalSpan = nameSpan + NAME_ID_GAP + idSpan;
|
|
271
|
-
const firstY = Math.round(opts.boxY + (opts.boxHeight - totalSpan) / 2);
|
|
345
|
+
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
346
|
+
const firstY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
272
347
|
|
|
273
348
|
const out: TextLineSpec[] = [];
|
|
274
349
|
for (let i = 0; i < nameLines.length; i++) {
|
|
275
350
|
out.push({ cls: 'text-primary', text: nameLines[i], y: firstY + i * LINE_H_PRIMARY });
|
|
276
351
|
}
|
|
277
|
-
const idFirstY = firstY +
|
|
352
|
+
const idFirstY = firstY + nameCenterSpan + NAME_ID_GAP;
|
|
278
353
|
for (let i = 0; i < idLines.length; i++) {
|
|
279
354
|
out.push({ cls: 'text-id', text: idLines[i], y: idFirstY + i * LINE_H_ID });
|
|
280
355
|
}
|
|
@@ -291,6 +366,14 @@ export interface LayoutHeaderBlockTextOptions {
|
|
|
291
366
|
marginX?: number;
|
|
292
367
|
}
|
|
293
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Direct centre-to-centre gap between the header's name and id lines.
|
|
371
|
+
* Safely above the ~11px non-overlap floor (half of text-primary's 12px font
|
|
372
|
+
* + half of text-id's 10px font, per the #419 regression) but tighter than
|
|
373
|
+
* {@link NAME_ID_GAP} to fit the compact header strip.
|
|
374
|
+
*/
|
|
375
|
+
const HEADER_NAME_ID_GAP = 12;
|
|
376
|
+
|
|
294
377
|
/** Container header: single-line name + id stacked in the header strip. */
|
|
295
378
|
export function layoutHeaderBlockText(opts: LayoutHeaderBlockTextOptions): TextLineSpec[] {
|
|
296
379
|
const marginX = opts.marginX ?? TEXT_MARGIN_X;
|
|
@@ -299,11 +382,14 @@ export function layoutHeaderBlockText(opts: LayoutHeaderBlockTextOptions): TextL
|
|
|
299
382
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
300
383
|
const nameText = truncateLine(opts.name, maxCharsName);
|
|
301
384
|
const idText = truncateId(opts.id, maxCharsId);
|
|
302
|
-
|
|
303
|
-
const
|
|
385
|
+
|
|
386
|
+
const contentSpan = HEADER_NAME_ID_GAP + HALF_EXTENT_PRIMARY + HALF_EXTENT_ID;
|
|
387
|
+
const margin = Math.max(0, (opts.headerHeight - contentSpan) / 2);
|
|
388
|
+
const nameY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
389
|
+
const idY = nameY + HEADER_NAME_ID_GAP;
|
|
304
390
|
return [
|
|
305
|
-
{ cls: 'text-primary', text: nameText, y:
|
|
306
|
-
{ cls: 'text-id', text: idText, y:
|
|
391
|
+
{ cls: 'text-primary', text: nameText, y: nameY },
|
|
392
|
+
{ cls: 'text-id', text: idText, y: idY },
|
|
307
393
|
];
|
|
308
394
|
}
|
|
309
395
|
|