@transitrix/diagrams 1.13.1 → 1.13.2
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/fgca/preview-layout.js +1 -0
- package/dist/webview/entity-text-layout.d.ts +2 -1
- package/dist/webview/entity-text-layout.js +26 -13
- package/dist/webview/render-fgca.js +6 -2
- package/package.json +1 -1
- package/src/fgca/preview-layout.ts +1 -0
- package/src/webview/entity-text-layout.ts +27 -14
- package/src/webview/render-fgca.ts +6 -2
|
@@ -235,6 +235,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
235
235
|
const coveredActivities = new Set(changes.flatMap(c => c.activity_ids.map(String)));
|
|
236
236
|
// Synthesize virtual Change nodes for activities without a real Change.
|
|
237
237
|
// Each virtual node is 1:1 with one activity (not shared across activities).
|
|
238
|
+
// The layout key embeds the activity id for uniqueness; it is not a display id.
|
|
238
239
|
const virtualChanges = [];
|
|
239
240
|
for (const a of doc.activities) {
|
|
240
241
|
if (a.goal_id != null && !coveredActivities.has(String(a.id))) {
|
|
@@ -64,7 +64,8 @@ export interface LayoutCenteredEntityOptions {
|
|
|
64
64
|
boxHeight: number;
|
|
65
65
|
name: string;
|
|
66
66
|
type?: string;
|
|
67
|
-
id
|
|
67
|
+
/** Canonical entity id. Empty or omitted skips the id row (placeholder nodes). */
|
|
68
|
+
id?: string;
|
|
68
69
|
nameMaxLines?: number;
|
|
69
70
|
idMaxLines?: number;
|
|
70
71
|
marginX?: number;
|
|
@@ -159,7 +159,15 @@ export function truncateId(id, maxChars) {
|
|
|
159
159
|
* to avoid overlap.
|
|
160
160
|
*/
|
|
161
161
|
const HALF_EXTENT_PRIMARY = 6; // text-primary, 12px font
|
|
162
|
+
const HALF_EXTENT_SECONDARY = 6; // text-secondary, same size as name
|
|
162
163
|
const HALF_EXTENT_ID = 5; // text-id, 10px font
|
|
164
|
+
function glyphHalfExtent(cls) {
|
|
165
|
+
if (cls === 'text-id')
|
|
166
|
+
return HALF_EXTENT_ID;
|
|
167
|
+
if (cls === 'text-secondary')
|
|
168
|
+
return HALF_EXTENT_SECONDARY;
|
|
169
|
+
return HALF_EXTENT_PRIMARY;
|
|
170
|
+
}
|
|
163
171
|
/** Place `groups`' lines starting at `startY`, advancing by each line's own height and gap. */
|
|
164
172
|
function placeLines(groups, startY) {
|
|
165
173
|
const out = [];
|
|
@@ -201,15 +209,18 @@ export function layoutCenteredEntityText(opts) {
|
|
|
201
209
|
const maxCharsType = maxCharsForInnerWidth(innerW, CHAR_W_SECONDARY);
|
|
202
210
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
203
211
|
const typeLine = opts.type?.trim() ? truncateLine(opts.type, maxCharsType) : undefined;
|
|
212
|
+
const idText = opts.id?.trim() ?? '';
|
|
204
213
|
const idMaxLines = opts.idMaxLines ?? 1;
|
|
205
|
-
const idLines =
|
|
214
|
+
const idLines = idText
|
|
215
|
+
? (idMaxLines <= 1 ? [truncateId(idText, maxCharsId)] : wrapId(idText, maxCharsId, idMaxLines))
|
|
216
|
+
: [];
|
|
206
217
|
function buildGroups(nameLines) {
|
|
207
218
|
const groups = [
|
|
208
219
|
{
|
|
209
220
|
cls: 'text-primary',
|
|
210
221
|
lines: nameLines,
|
|
211
222
|
lineHeight: LINE_H_PRIMARY,
|
|
212
|
-
gapAfter: typeLine ? ROW_GROUP_GAP : NAME_ID_GAP,
|
|
223
|
+
gapAfter: typeLine ? ROW_GROUP_GAP : idLines.length > 0 ? NAME_ID_GAP : 0,
|
|
213
224
|
},
|
|
214
225
|
];
|
|
215
226
|
if (typeLine) {
|
|
@@ -217,28 +228,30 @@ export function layoutCenteredEntityText(opts) {
|
|
|
217
228
|
cls: 'text-secondary',
|
|
218
229
|
lines: [typeLine],
|
|
219
230
|
lineHeight: LINE_H_SECONDARY,
|
|
220
|
-
gapAfter: NAME_ID_GAP,
|
|
231
|
+
gapAfter: idLines.length > 0 ? NAME_ID_GAP : 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (idLines.length > 0) {
|
|
235
|
+
groups.push({
|
|
236
|
+
cls: 'text-id',
|
|
237
|
+
lines: idLines,
|
|
238
|
+
lineHeight: LINE_H_ID,
|
|
239
|
+
gapAfter: 0,
|
|
221
240
|
});
|
|
222
241
|
}
|
|
223
|
-
groups.push({
|
|
224
|
-
cls: 'text-id',
|
|
225
|
-
lines: idLines,
|
|
226
|
-
lineHeight: LINE_H_ID,
|
|
227
|
-
gapAfter: 0,
|
|
228
|
-
});
|
|
229
242
|
return groups;
|
|
230
243
|
}
|
|
231
|
-
// groups[0] is always the name (text-primary)
|
|
232
|
-
//
|
|
244
|
+
// groups[0] is always the name (text-primary). The last group is the id when
|
|
245
|
+
// one is present, otherwise type or name; edge padding uses that class.
|
|
233
246
|
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 2);
|
|
234
247
|
const availableHeight = Math.max(0, opts.boxHeight - marginY * 2);
|
|
235
248
|
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
236
249
|
let groups = buildGroups(nameLines);
|
|
237
|
-
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY +
|
|
250
|
+
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + glyphHalfExtent(groups[groups.length - 1].cls);
|
|
238
251
|
for (let cap = requestedNameMaxLines - 1; cap >= 1 && contentSpan > availableHeight; cap--) {
|
|
239
252
|
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
240
253
|
groups = buildGroups(nameLines);
|
|
241
|
-
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY +
|
|
254
|
+
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + glyphHalfExtent(groups[groups.length - 1].cls);
|
|
242
255
|
}
|
|
243
256
|
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
244
257
|
const firstY = Math.round(opts.boxY + margin + HALF_EXTENT_PRIMARY);
|
|
@@ -48,7 +48,9 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
48
48
|
.join('\n');
|
|
49
49
|
const nodeSvg = nodes
|
|
50
50
|
.map((n) => {
|
|
51
|
-
|
|
51
|
+
// Virtual Change nodes use a layout key such as `vchange_activity_<id>`;
|
|
52
|
+
// slicing at the first `_` would print `activity_<id>` on a Change.
|
|
53
|
+
const entityId = n.virtual ? '' : n.id.slice(n.id.indexOf('_') + 1);
|
|
52
54
|
const nodeClass = n.virtual ? 'diagram-node layer-virtual' : `diagram-node layer-${n.col}`;
|
|
53
55
|
const specs = layoutCenteredEntityText({
|
|
54
56
|
boxX: n.x,
|
|
@@ -61,7 +63,9 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
61
63
|
idMaxLines: 1,
|
|
62
64
|
});
|
|
63
65
|
const textSvg = emitCenteredTextSvg(specs, n.x + nodeWidth / 2, escXml);
|
|
64
|
-
const titleContent =
|
|
66
|
+
const titleContent = n.virtual
|
|
67
|
+
? escXml(n.label)
|
|
68
|
+
: `${escXml(n.label)} (${escXml(entityId)})`;
|
|
65
69
|
const typeBadge = n.type && n.col === 'activity'
|
|
66
70
|
? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
|
|
67
71
|
: '';
|
package/package.json
CHANGED
|
@@ -371,6 +371,7 @@ export function layoutFGCAPreview(
|
|
|
371
371
|
|
|
372
372
|
// Synthesize virtual Change nodes for activities without a real Change.
|
|
373
373
|
// Each virtual node is 1:1 with one activity (not shared across activities).
|
|
374
|
+
// The layout key embeds the activity id for uniqueness; it is not a display id.
|
|
374
375
|
const virtualChanges: Array<{ id: string; activityId: string; goalId: string }> = [];
|
|
375
376
|
for (const a of doc.activities) {
|
|
376
377
|
if (a.goal_id != null && !coveredActivities.has(String(a.id))) {
|
|
@@ -165,7 +165,8 @@ export interface LayoutCenteredEntityOptions {
|
|
|
165
165
|
boxHeight: number;
|
|
166
166
|
name: string;
|
|
167
167
|
type?: string;
|
|
168
|
-
id
|
|
168
|
+
/** Canonical entity id. Empty or omitted skips the id row (placeholder nodes). */
|
|
169
|
+
id?: string;
|
|
169
170
|
nameMaxLines?: number;
|
|
170
171
|
idMaxLines?: number;
|
|
171
172
|
marginX?: number;
|
|
@@ -189,8 +190,15 @@ interface TextRowGroup {
|
|
|
189
190
|
* to avoid overlap.
|
|
190
191
|
*/
|
|
191
192
|
const HALF_EXTENT_PRIMARY = 6; // text-primary, 12px font
|
|
193
|
+
const HALF_EXTENT_SECONDARY = 6; // text-secondary, same size as name
|
|
192
194
|
const HALF_EXTENT_ID = 5; // text-id, 10px font
|
|
193
195
|
|
|
196
|
+
function glyphHalfExtent(cls: string): number {
|
|
197
|
+
if (cls === 'text-id') return HALF_EXTENT_ID;
|
|
198
|
+
if (cls === 'text-secondary') return HALF_EXTENT_SECONDARY;
|
|
199
|
+
return HALF_EXTENT_PRIMARY;
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
/** Place `groups`' lines starting at `startY`, advancing by each line's own height and gap. */
|
|
195
203
|
function placeLines(groups: TextRowGroup[], startY: number): TextLineSpec[] {
|
|
196
204
|
const out: TextLineSpec[] = [];
|
|
@@ -234,8 +242,11 @@ export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): Tex
|
|
|
234
242
|
const maxCharsId = maxCharsForInnerWidth(innerW, CHAR_W_ID);
|
|
235
243
|
|
|
236
244
|
const typeLine = opts.type?.trim() ? truncateLine(opts.type, maxCharsType) : undefined;
|
|
245
|
+
const idText = opts.id?.trim() ?? '';
|
|
237
246
|
const idMaxLines = opts.idMaxLines ?? 1;
|
|
238
|
-
const idLines =
|
|
247
|
+
const idLines = idText
|
|
248
|
+
? (idMaxLines <= 1 ? [truncateId(idText, maxCharsId)] : wrapId(idText, maxCharsId, idMaxLines))
|
|
249
|
+
: [];
|
|
239
250
|
|
|
240
251
|
function buildGroups(nameLines: string[]): TextRowGroup[] {
|
|
241
252
|
const groups: TextRowGroup[] = [
|
|
@@ -243,7 +254,7 @@ export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): Tex
|
|
|
243
254
|
cls: 'text-primary',
|
|
244
255
|
lines: nameLines,
|
|
245
256
|
lineHeight: LINE_H_PRIMARY,
|
|
246
|
-
gapAfter: typeLine ? ROW_GROUP_GAP : NAME_ID_GAP,
|
|
257
|
+
gapAfter: typeLine ? ROW_GROUP_GAP : idLines.length > 0 ? NAME_ID_GAP : 0,
|
|
247
258
|
},
|
|
248
259
|
];
|
|
249
260
|
if (typeLine) {
|
|
@@ -251,26 +262,28 @@ export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): Tex
|
|
|
251
262
|
cls: 'text-secondary',
|
|
252
263
|
lines: [typeLine],
|
|
253
264
|
lineHeight: LINE_H_SECONDARY,
|
|
254
|
-
gapAfter: NAME_ID_GAP,
|
|
265
|
+
gapAfter: idLines.length > 0 ? NAME_ID_GAP : 0,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (idLines.length > 0) {
|
|
269
|
+
groups.push({
|
|
270
|
+
cls: 'text-id',
|
|
271
|
+
lines: idLines,
|
|
272
|
+
lineHeight: LINE_H_ID,
|
|
273
|
+
gapAfter: 0,
|
|
255
274
|
});
|
|
256
275
|
}
|
|
257
|
-
groups.push({
|
|
258
|
-
cls: 'text-id',
|
|
259
|
-
lines: idLines,
|
|
260
|
-
lineHeight: LINE_H_ID,
|
|
261
|
-
gapAfter: 0,
|
|
262
|
-
});
|
|
263
276
|
return groups;
|
|
264
277
|
}
|
|
265
278
|
|
|
266
|
-
// groups[0] is always the name (text-primary)
|
|
267
|
-
//
|
|
279
|
+
// groups[0] is always the name (text-primary). The last group is the id when
|
|
280
|
+
// one is present, otherwise type or name; edge padding uses that class.
|
|
268
281
|
const requestedNameMaxLines = Math.max(1, opts.nameMaxLines ?? 2);
|
|
269
282
|
const availableHeight = Math.max(0, opts.boxHeight - marginY * 2);
|
|
270
283
|
|
|
271
284
|
let nameLines = wrapWords(opts.name, maxCharsName, requestedNameMaxLines);
|
|
272
285
|
let groups = buildGroups(nameLines);
|
|
273
|
-
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY +
|
|
286
|
+
let contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + glyphHalfExtent(groups[groups.length - 1].cls);
|
|
274
287
|
for (
|
|
275
288
|
let cap = requestedNameMaxLines - 1;
|
|
276
289
|
cap >= 1 && contentSpan > availableHeight;
|
|
@@ -278,7 +291,7 @@ export function layoutCenteredEntityText(opts: LayoutCenteredEntityOptions): Tex
|
|
|
278
291
|
) {
|
|
279
292
|
nameLines = wrapWords(opts.name, maxCharsName, cap);
|
|
280
293
|
groups = buildGroups(nameLines);
|
|
281
|
-
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY +
|
|
294
|
+
contentSpan = centerSpan(groups) + HALF_EXTENT_PRIMARY + glyphHalfExtent(groups[groups.length - 1].cls);
|
|
282
295
|
}
|
|
283
296
|
|
|
284
297
|
const margin = Math.max(0, (opts.boxHeight - contentSpan) / 2);
|
|
@@ -82,7 +82,9 @@ export function renderFgcaBody(
|
|
|
82
82
|
|
|
83
83
|
const nodeSvg = nodes
|
|
84
84
|
.map((n) => {
|
|
85
|
-
|
|
85
|
+
// Virtual Change nodes use a layout key such as `vchange_activity_<id>`;
|
|
86
|
+
// slicing at the first `_` would print `activity_<id>` on a Change.
|
|
87
|
+
const entityId = n.virtual ? '' : n.id.slice(n.id.indexOf('_') + 1);
|
|
86
88
|
const nodeClass = n.virtual ? 'diagram-node layer-virtual' : `diagram-node layer-${n.col}`;
|
|
87
89
|
const specs = layoutCenteredEntityText({
|
|
88
90
|
boxX: n.x,
|
|
@@ -95,7 +97,9 @@ export function renderFgcaBody(
|
|
|
95
97
|
idMaxLines: 1,
|
|
96
98
|
});
|
|
97
99
|
const textSvg = emitCenteredTextSvg(specs, n.x + nodeWidth / 2, escXml);
|
|
98
|
-
const titleContent =
|
|
100
|
+
const titleContent = n.virtual
|
|
101
|
+
? escXml(n.label)
|
|
102
|
+
: `${escXml(n.label)} (${escXml(entityId)})`;
|
|
99
103
|
const typeBadge = n.type && n.col === 'activity'
|
|
100
104
|
? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
|
|
101
105
|
: '';
|