compasso 0.4.1 → 0.5.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 +86 -5
- package/dist/{chunk-WJYYBGZW.js → chunk-2NDET6O5.js} +3 -3
- package/dist/{chunk-WJYYBGZW.js.map → chunk-2NDET6O5.js.map} +1 -1
- package/dist/{chunk-LR7BXUWM.js → chunk-3RGYLVTN.js} +3 -3
- package/dist/{chunk-LR7BXUWM.js.map → chunk-3RGYLVTN.js.map} +1 -1
- package/dist/chunk-BM7UJBK5.js +680 -0
- package/dist/chunk-BM7UJBK5.js.map +1 -0
- package/dist/chunk-DVLWT565.js +372 -0
- package/dist/chunk-DVLWT565.js.map +1 -0
- package/dist/{chunk-IPE7JZO5.js → chunk-JBDA7E2O.js} +3 -3
- package/dist/{chunk-IPE7JZO5.js.map → chunk-JBDA7E2O.js.map} +1 -1
- package/dist/chunk-MIJTBYX2.js +982 -0
- package/dist/chunk-MIJTBYX2.js.map +1 -0
- package/dist/{chunk-PGUMLTIM.js → chunk-PJHLWSGD.js} +3 -3
- package/dist/{chunk-PGUMLTIM.js.map → chunk-PJHLWSGD.js.map} +1 -1
- package/dist/{chunk-M4WA6ME7.js → chunk-RDH4XHA2.js} +3 -3
- package/dist/{chunk-M4WA6ME7.js.map → chunk-RDH4XHA2.js.map} +1 -1
- package/dist/{chunk-TAE2UB7D.js → chunk-WEHUSHVI.js} +4 -40
- package/dist/chunk-WEHUSHVI.js.map +1 -0
- package/dist/{chunk-FYBABYC7.js → chunk-Z66YUOUM.js} +3 -3
- package/dist/{chunk-FYBABYC7.js.map → chunk-Z66YUOUM.js.map} +1 -1
- package/dist/core/index.cjs +217 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +79 -3
- package/dist/core/index.d.ts +79 -3
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.d.cts +2 -2
- package/dist/fault-tree/index.d.ts +2 -2
- package/dist/fault-tree/index.js +2 -2
- package/dist/fishbone/index.js +2 -2
- package/dist/genogram/index.d.cts +2 -2
- package/dist/genogram/index.d.ts +2 -2
- package/dist/genogram/index.js +2 -2
- package/dist/geometry-P-XGqGe7.d.cts +8 -0
- package/dist/geometry-P-XGqGe7.d.ts +8 -0
- package/dist/grid-BMgUSly1.d.cts +79 -0
- package/dist/grid-BMgUSly1.d.ts +79 -0
- package/dist/index.cjs +2263 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +10 -8
- package/dist/labels-Br8yjc3C.d.cts +29 -0
- package/dist/labels-Br8yjc3C.d.ts +29 -0
- package/dist/labels-D1v1RWZd.d.cts +97 -0
- package/dist/labels-D1v1RWZd.d.ts +97 -0
- package/dist/layered-DmZluAqe.d.cts +72 -0
- package/dist/layered-DmZluAqe.d.ts +72 -0
- package/dist/locales/pt-br.cjs +53 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +7 -1
- package/dist/locales/pt-br.d.ts +7 -1
- package/dist/locales/pt-br.js +50 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/org-chart/index.cjs +38 -36
- package/dist/org-chart/index.cjs.map +1 -1
- package/dist/org-chart/index.d.cts +5 -31
- package/dist/org-chart/index.d.ts +5 -31
- package/dist/org-chart/index.js +2 -2
- package/dist/pedigree/index.d.cts +2 -2
- package/dist/pedigree/index.d.ts +2 -2
- package/dist/pedigree/index.js +2 -2
- package/dist/phylo/index.d.cts +2 -2
- package/dist/phylo/index.d.ts +2 -2
- package/dist/phylo/index.js +2 -2
- package/dist/prisma/index.cjs +882 -0
- package/dist/prisma/index.cjs.map +1 -0
- package/dist/prisma/index.d.cts +174 -0
- package/dist/prisma/index.d.ts +174 -0
- package/dist/prisma/index.js +4 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/{text-DuO_PwYw.d.cts → text-DDVzpwPZ.d.cts} +1 -8
- package/dist/{text-DuO_PwYw.d.ts → text-DDVzpwPZ.d.ts} +1 -8
- package/dist/uml/index.cjs +1214 -0
- package/dist/uml/index.cjs.map +1 -0
- package/dist/uml/index.d.cts +189 -0
- package/dist/uml/index.d.ts +189 -0
- package/dist/uml/index.js +4 -0
- package/dist/uml/index.js.map +1 -0
- package/package.json +28 -2
- package/dist/chunk-SD4NTRBM.js +0 -171
- package/dist/chunk-SD4NTRBM.js.map +0 -1
- package/dist/chunk-TAE2UB7D.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -168,6 +168,207 @@ function annotationSwatch(x, yCenter) {
|
|
|
168
168
|
return annotationDot(x + LEGEND_SWATCH_W / 2, yCenter);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// src/core/layered.ts
|
|
172
|
+
function packSubtree(node, gaps) {
|
|
173
|
+
const { ownHalfL, ownHalfR, children } = node;
|
|
174
|
+
if (children.length === 0) {
|
|
175
|
+
return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
|
|
176
|
+
}
|
|
177
|
+
const xs = [0];
|
|
178
|
+
for (let i = 1; i < children.length; i++) {
|
|
179
|
+
xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
|
|
180
|
+
}
|
|
181
|
+
const first = children[0];
|
|
182
|
+
const last = children[children.length - 1];
|
|
183
|
+
const axis = (xs[0] + xs[xs.length - 1]) / 2;
|
|
184
|
+
const offsets = xs.map((x) => x - axis);
|
|
185
|
+
const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
|
|
186
|
+
const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
|
|
187
|
+
return { halfL, halfR, offsets };
|
|
188
|
+
}
|
|
189
|
+
function allocateLanes(items) {
|
|
190
|
+
const lanes = [];
|
|
191
|
+
for (const it of items) {
|
|
192
|
+
let chosen = -1;
|
|
193
|
+
for (let l = 0; l < lanes.length; l++) {
|
|
194
|
+
if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
|
|
195
|
+
chosen = l;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (chosen === -1) {
|
|
200
|
+
chosen = lanes.length;
|
|
201
|
+
lanes.push([]);
|
|
202
|
+
}
|
|
203
|
+
lanes[chosen].push({ lo: it.lo, hi: it.hi });
|
|
204
|
+
it.set(chosen);
|
|
205
|
+
}
|
|
206
|
+
return lanes.length;
|
|
207
|
+
}
|
|
208
|
+
function bandStack(rows, opts) {
|
|
209
|
+
const { padding, corridor } = opts;
|
|
210
|
+
if (rows.length === 0) {
|
|
211
|
+
return { rowTop: [], busY: () => 0, height: padding * 2 };
|
|
212
|
+
}
|
|
213
|
+
const rowTop = [padding];
|
|
214
|
+
for (let d = 0; d < rows.length - 1; d++) {
|
|
215
|
+
rowTop.push(rowTop[d] + rows[d].rowH + rows[d].zoneH + corridor);
|
|
216
|
+
}
|
|
217
|
+
const last = rows.length - 1;
|
|
218
|
+
const busY = (d) => rowTop[d + 1] - corridor / 2;
|
|
219
|
+
const height = rowTop[last] + rows[last].rowH + rows[last].zoneH + padding;
|
|
220
|
+
return { rowTop, busY, height };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/core/glyph.ts
|
|
224
|
+
function dirVec(dir) {
|
|
225
|
+
switch (dir) {
|
|
226
|
+
case "right":
|
|
227
|
+
return [1, 0];
|
|
228
|
+
case "left":
|
|
229
|
+
return [-1, 0];
|
|
230
|
+
case "down":
|
|
231
|
+
return [0, 1];
|
|
232
|
+
case "up":
|
|
233
|
+
return [0, -1];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function r(n) {
|
|
237
|
+
return Math.round(n * 100) / 100;
|
|
238
|
+
}
|
|
239
|
+
function pt(x, y) {
|
|
240
|
+
return `${r(x)},${r(y)}`;
|
|
241
|
+
}
|
|
242
|
+
var GLYPH_ARROW_LEN = 10;
|
|
243
|
+
var GLYPH_ARROW_HALF = 5;
|
|
244
|
+
var GLYPH_TRI_LEN = 14;
|
|
245
|
+
var GLYPH_TRI_HALF = 8;
|
|
246
|
+
var GLYPH_DIAMOND_LEN = 16;
|
|
247
|
+
var GLYPH_DIAMOND_HALF = 6;
|
|
248
|
+
function arrowOpenPoints(tip, dir, len = GLYPH_ARROW_LEN, half = GLYPH_ARROW_HALF) {
|
|
249
|
+
return _arrowPoints(tip, dir, len, half);
|
|
250
|
+
}
|
|
251
|
+
function arrowFilledPoints(tip, dir, len = GLYPH_ARROW_LEN, half = GLYPH_ARROW_HALF) {
|
|
252
|
+
return _arrowPoints(tip, dir, len, half);
|
|
253
|
+
}
|
|
254
|
+
function _arrowPoints(tip, dir, len, half) {
|
|
255
|
+
const [ux, uy] = dirVec(dir);
|
|
256
|
+
const tx = tip.x;
|
|
257
|
+
const ty = tip.y;
|
|
258
|
+
const bx = tx - ux * len;
|
|
259
|
+
const by = ty - uy * len;
|
|
260
|
+
const px = -uy;
|
|
261
|
+
const py = ux;
|
|
262
|
+
return [
|
|
263
|
+
pt(tx, ty),
|
|
264
|
+
pt(bx + px * half, by + py * half),
|
|
265
|
+
pt(bx - px * half, by - py * half)
|
|
266
|
+
].join(" ");
|
|
267
|
+
}
|
|
268
|
+
function trianglePoints(tip, dir, len = GLYPH_TRI_LEN, half = GLYPH_TRI_HALF) {
|
|
269
|
+
return _arrowPoints(tip, dir, len, half);
|
|
270
|
+
}
|
|
271
|
+
function diamondPoints(tip, dir, len = GLYPH_DIAMOND_LEN, half = GLYPH_DIAMOND_HALF) {
|
|
272
|
+
const [ux, uy] = dirVec(dir);
|
|
273
|
+
const tx = tip.x;
|
|
274
|
+
const ty = tip.y;
|
|
275
|
+
const mx = tx + ux * (len / 2);
|
|
276
|
+
const my = ty + uy * (len / 2);
|
|
277
|
+
const basex = tx + ux * len;
|
|
278
|
+
const basey = ty + uy * len;
|
|
279
|
+
const px = -uy;
|
|
280
|
+
const py = ux;
|
|
281
|
+
return [
|
|
282
|
+
pt(tx, ty),
|
|
283
|
+
pt(mx - px * half, my - py * half),
|
|
284
|
+
pt(basex, basey),
|
|
285
|
+
pt(mx + px * half, my + py * half)
|
|
286
|
+
].join(" ");
|
|
287
|
+
}
|
|
288
|
+
function glyphInset(_dir, kind) {
|
|
289
|
+
switch (kind) {
|
|
290
|
+
case "arrow":
|
|
291
|
+
return GLYPH_ARROW_LEN;
|
|
292
|
+
case "triangle":
|
|
293
|
+
return GLYPH_TRI_LEN;
|
|
294
|
+
case "diamond":
|
|
295
|
+
return GLYPH_DIAMOND_LEN;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/core/compartment.ts
|
|
300
|
+
function round2(n) {
|
|
301
|
+
return Math.round(n * 100) / 100;
|
|
302
|
+
}
|
|
303
|
+
function measureCompartmentBox(compartments, opts) {
|
|
304
|
+
const { padX, padY, lineH, minW } = opts;
|
|
305
|
+
let maxLineW = 0;
|
|
306
|
+
for (const c of compartments) {
|
|
307
|
+
for (const line of c.lines) {
|
|
308
|
+
const w = estimateTextWidth(line, c.font);
|
|
309
|
+
if (w > maxLineW) maxLineW = w;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const boxW = round2(Math.max(minW, maxLineW + 2 * padX));
|
|
313
|
+
const rows = [];
|
|
314
|
+
const dividerYs = [];
|
|
315
|
+
let y = 0;
|
|
316
|
+
for (let i = 0; i < compartments.length; i++) {
|
|
317
|
+
const c = compartments[i];
|
|
318
|
+
const lineCount = c.lines.length > 0 ? c.lines.length : 1;
|
|
319
|
+
const compartmentH = padY + lineCount * lineH + padY;
|
|
320
|
+
rows.push({
|
|
321
|
+
lines: c.lines,
|
|
322
|
+
top: round2(y + padY),
|
|
323
|
+
font: c.font
|
|
324
|
+
});
|
|
325
|
+
y += compartmentH;
|
|
326
|
+
if (i < compartments.length - 1) {
|
|
327
|
+
dividerYs.push(round2(y));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const boxH = round2(y);
|
|
331
|
+
return { boxW, boxH, rows, dividerYs };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/core/grid.ts
|
|
335
|
+
function round3(n) {
|
|
336
|
+
return Math.round(n * 100) / 100;
|
|
337
|
+
}
|
|
338
|
+
function packGrid(cells, gaps) {
|
|
339
|
+
if (cells.length === 0) {
|
|
340
|
+
return { cellX: [], cellY: [], colW: [], rowH: [] };
|
|
341
|
+
}
|
|
342
|
+
const { colGap, rowGap } = gaps;
|
|
343
|
+
let maxCol = 0;
|
|
344
|
+
let maxRow = 0;
|
|
345
|
+
for (const c of cells) {
|
|
346
|
+
if (c.col > maxCol) maxCol = c.col;
|
|
347
|
+
if (c.row > maxRow) maxRow = c.row;
|
|
348
|
+
}
|
|
349
|
+
const colW = Array.from({ length: maxCol + 1 }, () => 0);
|
|
350
|
+
const rowH = Array.from({ length: maxRow + 1 }, () => 0);
|
|
351
|
+
for (const c of cells) {
|
|
352
|
+
if (c.w > colW[c.col]) colW[c.col] = c.w;
|
|
353
|
+
if (c.h > rowH[c.row]) rowH[c.row] = c.h;
|
|
354
|
+
}
|
|
355
|
+
const colX = Array.from({ length: maxCol + 1 }, () => 0);
|
|
356
|
+
for (let c = 1; c <= maxCol; c++) {
|
|
357
|
+
colX[c] = round3(colX[c - 1] + colW[c - 1] + colGap);
|
|
358
|
+
}
|
|
359
|
+
const rowY = Array.from({ length: maxRow + 1 }, () => 0);
|
|
360
|
+
for (let r2 = 1; r2 <= maxRow; r2++) {
|
|
361
|
+
rowY[r2] = round3(rowY[r2 - 1] + rowH[r2 - 1] + rowGap);
|
|
362
|
+
}
|
|
363
|
+
const cellX = cells.map(
|
|
364
|
+
(c) => round3(colX[c.col] + colW[c.col] / 2)
|
|
365
|
+
);
|
|
366
|
+
const cellY = cells.map(
|
|
367
|
+
(c) => round3(rowY[c.row] + rowH[c.row] / 2)
|
|
368
|
+
);
|
|
369
|
+
return { cellX, cellY, colW, rowH };
|
|
370
|
+
}
|
|
371
|
+
|
|
171
372
|
// src/genogram/types.ts
|
|
172
373
|
var UNION_STATUSES = [
|
|
173
374
|
"married",
|
|
@@ -302,7 +503,7 @@ function arrivalOffset(slot, total) {
|
|
|
302
503
|
const sign = slot % 2 === 1 ? -1 : 1;
|
|
303
504
|
return sign * Math.ceil(slot / 2) * step;
|
|
304
505
|
}
|
|
305
|
-
function
|
|
506
|
+
function allocateLanes2(items) {
|
|
306
507
|
const lanes = [];
|
|
307
508
|
for (const it of items) {
|
|
308
509
|
let chosen = -1;
|
|
@@ -333,12 +534,12 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
333
534
|
const validUnions = unions.filter((u) => ids.has(u.personAId) && ids.has(u.personBId) && u.personAId !== u.personBId).sort((a, b) => a.id - b.id);
|
|
334
535
|
const coupleUnions = validUnions.filter((u) => u.status !== "coparental");
|
|
335
536
|
const validLinks = parentLinks.filter((l) => ids.has(l.parentId) && ids.has(l.childId) && l.parentId !== l.childId).sort((a, b) => a.id - b.id);
|
|
336
|
-
const validRels = relationships.filter((
|
|
537
|
+
const validRels = relationships.filter((r2) => ids.has(r2.fromPersonId) && ids.has(r2.toPersonId) && r2.fromPersonId !== r2.toPersonId).sort((a, b) => a.id - b.id);
|
|
337
538
|
const relClass = new Map(
|
|
338
|
-
validRels.map((
|
|
539
|
+
validRels.map((r2) => [r2.id, classifyRelationshipType(r2.type, kinship)])
|
|
339
540
|
);
|
|
340
|
-
const bondRels = validRels.filter((
|
|
341
|
-
const parentageRels = validRels.filter((
|
|
541
|
+
const bondRels = validRels.filter((r2) => relClass.get(r2.id) === "bond");
|
|
542
|
+
const parentageRels = validRels.filter((r2) => relClass.get(r2.id) === "parentage");
|
|
342
543
|
const realLinks = validLinks.map((l) => ({
|
|
343
544
|
parentId: l.parentId,
|
|
344
545
|
childId: l.childId,
|
|
@@ -348,22 +549,22 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
348
549
|
}));
|
|
349
550
|
const declaredPairs = new Set(validLinks.map((l) => pairKey(l.parentId, l.childId)));
|
|
350
551
|
const promotedByPair = /* @__PURE__ */ new Map();
|
|
351
|
-
for (const
|
|
352
|
-
const key = pairKey(
|
|
552
|
+
for (const r2 of parentageRels) {
|
|
553
|
+
const key = pairKey(r2.fromPersonId, r2.toPersonId);
|
|
353
554
|
if (declaredPairs.has(key)) continue;
|
|
354
555
|
if (promotedByPair.has(key)) continue;
|
|
355
|
-
const ga = genById.get(
|
|
356
|
-
const gb = genById.get(
|
|
556
|
+
const ga = genById.get(r2.fromPersonId) ?? null;
|
|
557
|
+
const gb = genById.get(r2.toPersonId) ?? null;
|
|
357
558
|
let parentId;
|
|
358
559
|
let childId;
|
|
359
560
|
if (ga !== null && gb !== null && ga !== gb) {
|
|
360
|
-
[parentId, childId] = ga < gb ? [
|
|
561
|
+
[parentId, childId] = ga < gb ? [r2.fromPersonId, r2.toPersonId] : [r2.toPersonId, r2.fromPersonId];
|
|
361
562
|
} else {
|
|
362
|
-
const fromIsChild = relationshipTypeTokens(
|
|
363
|
-
[parentId, childId] = fromIsChild ? [
|
|
563
|
+
const fromIsChild = relationshipTypeTokens(r2.type).some((t) => kinship.childWords.has(t));
|
|
564
|
+
[parentId, childId] = fromIsChild ? [r2.toPersonId, r2.fromPersonId] : [r2.fromPersonId, r2.toPersonId];
|
|
364
565
|
}
|
|
365
566
|
if (parentId === childId) continue;
|
|
366
|
-
promotedByPair.set(key, { parentId, childId, quality:
|
|
567
|
+
promotedByPair.set(key, { parentId, childId, quality: r2.quality, edgeId: PROMOTED_REL_ID_BASE + r2.id, annotated: r2.annotated ?? false });
|
|
367
568
|
}
|
|
368
569
|
const allLinks = [...realLinks, ...promotedByPair.values()].sort((a, b) => a.edgeId - b.edgeId);
|
|
369
570
|
const coupleByPair = /* @__PURE__ */ new Map();
|
|
@@ -385,9 +586,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
385
586
|
hasTie.add(l.parentId);
|
|
386
587
|
hasTie.add(l.childId);
|
|
387
588
|
}
|
|
388
|
-
for (const
|
|
389
|
-
hasTie.add(
|
|
390
|
-
hasTie.add(
|
|
589
|
+
for (const r2 of bondRels) {
|
|
590
|
+
hasTie.add(r2.fromPersonId);
|
|
591
|
+
hasTie.add(r2.toPersonId);
|
|
391
592
|
}
|
|
392
593
|
const isIsolated = (id) => !hasTie.has(id);
|
|
393
594
|
const byGen = /* @__PURE__ */ new Map();
|
|
@@ -403,13 +604,13 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
403
604
|
});
|
|
404
605
|
const rowCount = genKeys.length;
|
|
405
606
|
const rowOfPerson = /* @__PURE__ */ new Map();
|
|
406
|
-
genKeys.forEach((g,
|
|
407
|
-
const rowBlocks = genKeys.map((g,
|
|
607
|
+
genKeys.forEach((g, r2) => byGen.get(g).forEach((p) => rowOfPerson.set(p.id, r2)));
|
|
608
|
+
const rowBlocks = genKeys.map((g, r2) => {
|
|
408
609
|
const members = byGen.get(g).map((p) => p.id);
|
|
409
610
|
const adj = /* @__PURE__ */ new Map();
|
|
410
611
|
for (const id of members) adj.set(id, []);
|
|
411
612
|
for (const u of coupleUnions) {
|
|
412
|
-
if (rowOfPerson.get(u.personAId) ===
|
|
613
|
+
if (rowOfPerson.get(u.personAId) === r2 && rowOfPerson.get(u.personBId) === r2) {
|
|
413
614
|
adj.get(u.personAId).push(u.personBId);
|
|
414
615
|
adj.get(u.personBId).push(u.personAId);
|
|
415
616
|
}
|
|
@@ -465,9 +666,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
465
666
|
for (const rl of bondRels) if (rl.fromPersonId === id && rl.toPersonId === other || rl.toPersonId === id && rl.fromPersonId === other) w = Math.max(w, 0.5);
|
|
466
667
|
return w;
|
|
467
668
|
};
|
|
468
|
-
const sweep = (
|
|
669
|
+
const sweep = (r2, ref) => {
|
|
469
670
|
const pos = flattened();
|
|
470
|
-
const blocks = rowBlocks[
|
|
671
|
+
const blocks = rowBlocks[r2];
|
|
471
672
|
const bcOf = (b) => {
|
|
472
673
|
let sum = 0;
|
|
473
674
|
let wsum = 0;
|
|
@@ -483,23 +684,23 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
483
684
|
};
|
|
484
685
|
const keyed = blocks.map((b) => ({ b, bc: bcOf(b), minId: Math.min(...b) }));
|
|
485
686
|
keyed.sort((x, y) => x.bc !== y.bc ? x.bc - y.bc : x.minId - y.minId);
|
|
486
|
-
rowBlocks[
|
|
687
|
+
rowBlocks[r2] = keyed.map((k) => k.b);
|
|
487
688
|
};
|
|
488
689
|
if (rowCount >= 2) {
|
|
489
|
-
for (let
|
|
490
|
-
for (let
|
|
690
|
+
for (let r2 = 1; r2 < rowCount; r2++) sweep(r2, r2 - 1);
|
|
691
|
+
for (let r2 = rowCount - 2; r2 >= 0; r2--) sweep(r2, r2 + 1);
|
|
491
692
|
}
|
|
492
|
-
for (let
|
|
493
|
-
const blocks = rowBlocks[
|
|
693
|
+
for (let r2 = 0; r2 < rowCount; r2++) {
|
|
694
|
+
const blocks = rowBlocks[r2];
|
|
494
695
|
const connected = blocks.filter((b) => !(b.length === 1 && isIsolated(b[0])));
|
|
495
696
|
const isolated = blocks.filter((b) => b.length === 1 && isIsolated(b[0])).sort((a, b) => a[0] - b[0]);
|
|
496
|
-
rowBlocks[
|
|
697
|
+
rowBlocks[r2] = [...connected, ...isolated];
|
|
497
698
|
}
|
|
498
699
|
const colOf = /* @__PURE__ */ new Map();
|
|
499
700
|
let colCount = 0;
|
|
500
|
-
for (let
|
|
701
|
+
for (let r2 = 0; r2 < rowCount; r2++) {
|
|
501
702
|
let c = 0;
|
|
502
|
-
for (const b of rowBlocks[
|
|
703
|
+
for (const b of rowBlocks[r2]) for (const id of b) colOf.set(id, c++);
|
|
503
704
|
colCount = Math.max(colCount, c);
|
|
504
705
|
}
|
|
505
706
|
const colOrThrow = (id) => colOf.get(id);
|
|
@@ -526,8 +727,8 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
526
727
|
}
|
|
527
728
|
const rowMaxLines = Array.from({ length: rowCount }, () => 1);
|
|
528
729
|
for (const p of people) {
|
|
529
|
-
const
|
|
530
|
-
rowMaxLines[
|
|
730
|
+
const r2 = rowOfPerson.get(p.id);
|
|
731
|
+
rowMaxLines[r2] = Math.max(rowMaxLines[r2], measured.get(p.id).lines.length);
|
|
531
732
|
}
|
|
532
733
|
const planned = [];
|
|
533
734
|
const gutterReqs = [];
|
|
@@ -646,10 +847,10 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
646
847
|
{
|
|
647
848
|
const byRow = /* @__PURE__ */ new Map();
|
|
648
849
|
for (const u of barUnions) {
|
|
649
|
-
const
|
|
650
|
-
(byRow.get(
|
|
850
|
+
const r2 = rowOfPerson.get(u.personAId);
|
|
851
|
+
(byRow.get(r2) ?? byRow.set(r2, []).get(r2)).push(u);
|
|
651
852
|
}
|
|
652
|
-
for (const [
|
|
853
|
+
for (const [r2, rowUnions] of byRow) {
|
|
653
854
|
const unionsOfPerson = /* @__PURE__ */ new Map();
|
|
654
855
|
for (const u of rowUnions) {
|
|
655
856
|
(unionsOfPerson.get(u.personAId) ?? unionsOfPerson.set(u.personAId, []).get(u.personAId)).push(u);
|
|
@@ -675,7 +876,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
675
876
|
}
|
|
676
877
|
comp.sort((a, b) => a.id - b.id);
|
|
677
878
|
comp.forEach((u, i) => unionDipLevel.set(u.id, i));
|
|
678
|
-
rowDipLevels[
|
|
879
|
+
rowDipLevels[r2] = Math.max(rowDipLevels[r2], comp.length - 1);
|
|
679
880
|
}
|
|
680
881
|
}
|
|
681
882
|
}
|
|
@@ -900,19 +1101,19 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
900
1101
|
});
|
|
901
1102
|
}
|
|
902
1103
|
const bondGroups = /* @__PURE__ */ new Map();
|
|
903
|
-
for (const
|
|
904
|
-
const style = qualityLineStyle(
|
|
905
|
-
const lo = Math.min(
|
|
906
|
-
const hi = Math.max(
|
|
1104
|
+
for (const r2 of bondRels) {
|
|
1105
|
+
const style = qualityLineStyle(r2.quality, opts.qualityLexicon);
|
|
1106
|
+
const lo = Math.min(r2.fromPersonId, r2.toPersonId);
|
|
1107
|
+
const hi = Math.max(r2.fromPersonId, r2.toPersonId);
|
|
907
1108
|
const key = `${lo}|${hi}|${style}`;
|
|
908
|
-
const title =
|
|
1109
|
+
const title = r2.quality !== null ? `${r2.type} \xB7 ${r2.quality}` : r2.type;
|
|
909
1110
|
const g = bondGroups.get(key);
|
|
910
1111
|
if (g === void 0) {
|
|
911
|
-
bondGroups.set(key, { relIds: [
|
|
1112
|
+
bondGroups.set(key, { relIds: [r2.id], titles: [title], style, aId: r2.fromPersonId, bId: r2.toPersonId, annotated: r2.annotated ?? false });
|
|
912
1113
|
} else {
|
|
913
|
-
g.relIds.push(
|
|
1114
|
+
g.relIds.push(r2.id);
|
|
914
1115
|
g.titles.push(title);
|
|
915
|
-
if (
|
|
1116
|
+
if (r2.annotated) g.annotated = true;
|
|
916
1117
|
}
|
|
917
1118
|
}
|
|
918
1119
|
const bondList = [...bondGroups.values()].sort((a, b) => Math.max(...a.relIds) - Math.max(...b.relIds));
|
|
@@ -947,7 +1148,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
947
1148
|
const laneCount = (gutter, side) => gutterLaneCount.get(`${gutter}:${side}`) ?? 0;
|
|
948
1149
|
const geo = {
|
|
949
1150
|
cx: (c) => colCenterX[c],
|
|
950
|
-
cy: (
|
|
1151
|
+
cy: (r2) => rowCenterY[r2],
|
|
951
1152
|
glyphRight: (id) => colCenterX[colOrThrow(id)] + NODE_SIZE / 2,
|
|
952
1153
|
glyphLeft: (id) => colCenterX[colOrThrow(id)] - NODE_SIZE / 2,
|
|
953
1154
|
gutterCenterX: (g) => {
|
|
@@ -965,7 +1166,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
965
1166
|
},
|
|
966
1167
|
corridorLaneY: (req) => corridorTopY[req.corridor] + CORRIDOR_BASE / 2 + req.lane * LANE_H,
|
|
967
1168
|
/** Y of a dipped serial-union bar (level ≥ 1): below the row's glyph bottom edge. */
|
|
968
|
-
unionDipY: (
|
|
1169
|
+
unionDipY: (r2, level) => rowCenterY[r2] + NODE_SIZE / 2 + level * UNION_STAGGER
|
|
969
1170
|
};
|
|
970
1171
|
const gutterGroups = /* @__PURE__ */ new Map();
|
|
971
1172
|
for (const req of gutterReqs) {
|
|
@@ -975,7 +1176,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
975
1176
|
for (const [k, reqs] of gutterGroups) {
|
|
976
1177
|
gutterLaneCount.set(
|
|
977
1178
|
k,
|
|
978
|
-
|
|
1179
|
+
allocateLanes2(reqs.map((req) => ({ lo: req.rowLo, hi: req.rowHi, set: (lane) => req.lane = lane })))
|
|
979
1180
|
);
|
|
980
1181
|
}
|
|
981
1182
|
const gutterWidth = (g) => {
|
|
@@ -996,7 +1197,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
996
1197
|
const corridorGroups = /* @__PURE__ */ new Map();
|
|
997
1198
|
for (const req of corridorReqs) (corridorGroups.get(req.corridor) ?? corridorGroups.set(req.corridor, []).get(req.corridor)).push(req);
|
|
998
1199
|
for (const [k, reqs] of corridorGroups) {
|
|
999
|
-
corridorLaneCount[k] =
|
|
1200
|
+
corridorLaneCount[k] = allocateLanes2(
|
|
1000
1201
|
reqs.map((req) => {
|
|
1001
1202
|
const [lo, hi] = req.xRange();
|
|
1002
1203
|
return { lo, hi, set: (lane) => req.lane = lane };
|
|
@@ -1008,20 +1209,20 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
1008
1209
|
if (lanes > 0) return CORRIDOR_BASE + lanes * LANE_H;
|
|
1009
1210
|
return k < rowCount - 1 ? CORRIDOR_BASE : 0;
|
|
1010
1211
|
};
|
|
1011
|
-
const rowDipReserve = (
|
|
1212
|
+
const rowDipReserve = (r2) => rowDipLevels[r2] > 0 ? rowDipLevels[r2] * UNION_STAGGER + 6 : 0;
|
|
1012
1213
|
let cursorY = PADDING;
|
|
1013
|
-
for (let
|
|
1014
|
-
rowCenterY[
|
|
1015
|
-
rowLabelTopY[
|
|
1016
|
-
cursorY = rowLabelTopY[
|
|
1017
|
-
corridorTopY[
|
|
1018
|
-
cursorY += corridorHeight(
|
|
1214
|
+
for (let r2 = 0; r2 < rowCount; r2++) {
|
|
1215
|
+
rowCenterY[r2] = cursorY + NODE_SIZE / 2;
|
|
1216
|
+
rowLabelTopY[r2] = cursorY + NODE_SIZE + rowDipReserve(r2) + LABEL_GAP_TOP;
|
|
1217
|
+
cursorY = rowLabelTopY[r2] + rowMaxLines[r2] * LABEL_LINE_H;
|
|
1218
|
+
corridorTopY[r2] = cursorY;
|
|
1219
|
+
cursorY += corridorHeight(r2);
|
|
1019
1220
|
}
|
|
1020
1221
|
const width = dims.width;
|
|
1021
1222
|
const height = cursorY + PADDING;
|
|
1022
1223
|
const nodes = [...people].sort((a, b) => a.id - b.id).map((p) => {
|
|
1023
1224
|
const m = measured.get(p.id);
|
|
1024
|
-
const
|
|
1225
|
+
const r2 = rowOfPerson.get(p.id);
|
|
1025
1226
|
const c = colOrThrow(p.id);
|
|
1026
1227
|
return {
|
|
1027
1228
|
personId: p.id,
|
|
@@ -1030,10 +1231,10 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
1030
1231
|
deceased: p.deceased,
|
|
1031
1232
|
annotated: p.annotated ?? false,
|
|
1032
1233
|
cx: colCenterX[c],
|
|
1033
|
-
cy: rowCenterY[
|
|
1234
|
+
cy: rowCenterY[r2],
|
|
1034
1235
|
size: NODE_SIZE,
|
|
1035
1236
|
labelLines: m.lines,
|
|
1036
|
-
labelTop: rowLabelTopY[
|
|
1237
|
+
labelTop: rowLabelTopY[r2]
|
|
1037
1238
|
};
|
|
1038
1239
|
});
|
|
1039
1240
|
const elements = planned.map((pl) => ({
|
|
@@ -1318,7 +1519,7 @@ var SINGLE_RING_MAX = 8;
|
|
|
1318
1519
|
var NODE_STROKE = "#52525b";
|
|
1319
1520
|
var LABEL_FILL = "#3f3f46";
|
|
1320
1521
|
var EDGE_INK2 = "#71717a";
|
|
1321
|
-
var
|
|
1522
|
+
var round4 = (n) => Math.round(n * 100) / 100;
|
|
1322
1523
|
function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
1323
1524
|
const LEN = 9;
|
|
1324
1525
|
const HALF_W = 4.5;
|
|
@@ -1327,9 +1528,9 @@ function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
|
1327
1528
|
const px = -uy;
|
|
1328
1529
|
const py = ux;
|
|
1329
1530
|
const points = [
|
|
1330
|
-
`${
|
|
1331
|
-
`${
|
|
1332
|
-
`${
|
|
1531
|
+
`${round4(tipX)},${round4(tipY)}`,
|
|
1532
|
+
`${round4(bx + px * HALF_W)},${round4(by + py * HALF_W)}`,
|
|
1533
|
+
`${round4(bx - px * HALF_W)},${round4(by - py * HALF_W)}`
|
|
1333
1534
|
].join(" ");
|
|
1334
1535
|
return `<polygon points="${points}" fill="${EDGE_INK2}" fill-opacity="${opacity}"/>`;
|
|
1335
1536
|
}
|
|
@@ -1375,16 +1576,16 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1375
1576
|
sameRingRadius(twoRings ? 2 : 1)
|
|
1376
1577
|
);
|
|
1377
1578
|
if (twoRings) {
|
|
1378
|
-
const crossDist = (
|
|
1579
|
+
const crossDist = (r2) => Math.sqrt(r2 * r2 + (r2 + ringStep) ** 2 - 2 * r2 * (r2 + ringStep) * Math.cos(stepAngle));
|
|
1379
1580
|
while (crossDist(innerR) < safeDist) innerR += 8;
|
|
1380
1581
|
}
|
|
1381
1582
|
const outerR = innerR + ringStep;
|
|
1382
1583
|
for (let i = 0; i < n; i++) {
|
|
1383
1584
|
const s = sats[i];
|
|
1384
1585
|
s.angle = -Math.PI / 2 + i * Math.PI * 2 / n;
|
|
1385
|
-
const
|
|
1386
|
-
s.x =
|
|
1387
|
-
s.y =
|
|
1586
|
+
const r2 = twoRings && i % 2 === 1 ? outerR : innerR;
|
|
1587
|
+
s.x = r2 * Math.cos(s.angle);
|
|
1588
|
+
s.y = r2 * Math.sin(s.angle);
|
|
1388
1589
|
}
|
|
1389
1590
|
let minX = -centerR;
|
|
1390
1591
|
let minY = -centerR;
|
|
@@ -1419,7 +1620,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1419
1620
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1420
1621
|
const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} \xB7 ${s.tie.quality}` : s.tie.label);
|
|
1421
1622
|
const body = [
|
|
1422
|
-
`<line x1="${
|
|
1623
|
+
`<line x1="${round4(x1)}" y1="${round4(y1)}" x2="${round4(x2)}" y2="${round4(y2)}" stroke="${EDGE_INK2}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`
|
|
1423
1624
|
];
|
|
1424
1625
|
if (s.tie.direction === "in" || s.tie.direction === "both") {
|
|
1425
1626
|
body.push(arrowHead(x2, y2, ux, uy, ink.opacity));
|
|
@@ -1431,19 +1632,19 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1431
1632
|
}
|
|
1432
1633
|
{
|
|
1433
1634
|
const tspans = centerLines.map(
|
|
1434
|
-
(line, i) => `<tspan x="${
|
|
1635
|
+
(line, i) => `<tspan x="${round4(cx)}" y="${round4(cy - (centerLines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
|
|
1435
1636
|
).join("");
|
|
1436
1637
|
parts.push(
|
|
1437
|
-
`<g data-individual-id="center"><title>${xmlEscape(input.centerLabel)}</title><circle cx="${
|
|
1638
|
+
`<g data-individual-id="center"><title>${xmlEscape(input.centerLabel)}</title><circle cx="${round4(cx)}" cy="${round4(cy)}" r="${round4(centerR)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="2"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
|
|
1438
1639
|
);
|
|
1439
1640
|
}
|
|
1440
1641
|
for (const s of sats) {
|
|
1441
1642
|
const tspans = s.lines.map(
|
|
1442
|
-
(line, i) => `<tspan x="${
|
|
1643
|
+
(line, i) => `<tspan x="${round4(s.x)}" y="${round4(s.y - (s.lines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
|
|
1443
1644
|
).join("");
|
|
1444
1645
|
const pieces = [
|
|
1445
1646
|
`<title>${xmlEscape(s.tie.label)}</title>`,
|
|
1446
|
-
`<ellipse cx="${
|
|
1647
|
+
`<ellipse cx="${round4(s.x)}" cy="${round4(s.y)}" rx="${round4(s.rx)}" ry="${round4(s.ry)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="1.5"/>`,
|
|
1447
1648
|
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text>`
|
|
1448
1649
|
];
|
|
1449
1650
|
if (s.tie.annotated) {
|
|
@@ -1654,7 +1855,7 @@ function faultTreeIssues(input) {
|
|
|
1654
1855
|
);
|
|
1655
1856
|
}
|
|
1656
1857
|
}
|
|
1657
|
-
const
|
|
1858
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
1658
1859
|
"duplicate-id",
|
|
1659
1860
|
"unknown-input",
|
|
1660
1861
|
"gate-on-non-intermediate",
|
|
@@ -1663,7 +1864,7 @@ function faultTreeIssues(input) {
|
|
|
1663
1864
|
"inhibit-condition",
|
|
1664
1865
|
"top-not-intermediate"
|
|
1665
1866
|
]);
|
|
1666
|
-
if (!issues.some((i) =>
|
|
1867
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
1667
1868
|
const gateOf = (eventId) => gates.find((g) => g.eventId === eventId);
|
|
1668
1869
|
const reachable = /* @__PURE__ */ new Set();
|
|
1669
1870
|
const queue = [input.topId];
|
|
@@ -1743,7 +1944,7 @@ var FT_DROP_ID_BASE = 2e6;
|
|
|
1743
1944
|
var FT_BUS_ID_BASE = 3e6;
|
|
1744
1945
|
var FT_RISER_ID_BASE = 4e6;
|
|
1745
1946
|
var FT_CONDITION_ID_BASE = 5e6;
|
|
1746
|
-
var
|
|
1947
|
+
var round5 = (n) => Math.round(n * 100) / 100;
|
|
1747
1948
|
function wrapLeafLabel(displayLabel) {
|
|
1748
1949
|
const perLine = Math.min(20, Math.max(12, Math.ceil(displayLabel.length / 2) + 2));
|
|
1749
1950
|
return wrapLabel(displayLabel, perLine);
|
|
@@ -1930,14 +2131,14 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1930
2131
|
eventId: inst.event.id,
|
|
1931
2132
|
kind: inst.event.kind,
|
|
1932
2133
|
instance: instanceOf(inst),
|
|
1933
|
-
cx:
|
|
1934
|
-
top:
|
|
1935
|
-
nodeW:
|
|
1936
|
-
nodeH:
|
|
1937
|
-
glyphW:
|
|
1938
|
-
glyphH:
|
|
2134
|
+
cx: round5(inst.cx),
|
|
2135
|
+
top: round5(top),
|
|
2136
|
+
nodeW: round5(inst.m.nodeW),
|
|
2137
|
+
nodeH: round5(inst.m.nodeH),
|
|
2138
|
+
glyphW: round5(inst.m.glyphW),
|
|
2139
|
+
glyphH: round5(inst.m.glyphH),
|
|
1939
2140
|
labelLines: inst.m.labelLines,
|
|
1940
|
-
labelTop: labelTop === null ? null :
|
|
2141
|
+
labelTop: labelTop === null ? null : round5(labelTop),
|
|
1941
2142
|
code: inst.m.code,
|
|
1942
2143
|
depth: inst.depth,
|
|
1943
2144
|
title: inst.title
|
|
@@ -1955,8 +2156,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1955
2156
|
gates.push({
|
|
1956
2157
|
gateId: g.id,
|
|
1957
2158
|
type: g.type,
|
|
1958
|
-
cx:
|
|
1959
|
-
top:
|
|
2159
|
+
cx: round5(inst.cx),
|
|
2160
|
+
top: round5(gateTop),
|
|
1960
2161
|
glyphW,
|
|
1961
2162
|
glyphH,
|
|
1962
2163
|
title,
|
|
@@ -1973,8 +2174,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1973
2174
|
edgeId: FT_CONDITION_ID_BASE + g.id,
|
|
1974
2175
|
kind: "condition",
|
|
1975
2176
|
points: [
|
|
1976
|
-
{ x:
|
|
1977
|
-
{ x:
|
|
2177
|
+
{ x: round5(inst.cx + INHIBIT_W / 2), y: round5(gateTop + INHIBIT_CY) },
|
|
2178
|
+
{ x: round5(ovalLeft), y: round5(gateTop + INHIBIT_CY) }
|
|
1978
2179
|
],
|
|
1979
2180
|
instance: null,
|
|
1980
2181
|
title: titleLabels.condition
|
|
@@ -1984,8 +2185,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1984
2185
|
edgeId: FT_STEM_ID_BASE + g.id,
|
|
1985
2186
|
kind: "stem",
|
|
1986
2187
|
points: [
|
|
1987
|
-
{ x:
|
|
1988
|
-
{ x:
|
|
2188
|
+
{ x: round5(inst.cx), y: round5(rowTop[d] + inst.m.nodeH) },
|
|
2189
|
+
{ x: round5(inst.cx), y: round5(gateTop) }
|
|
1989
2190
|
],
|
|
1990
2191
|
instance: null,
|
|
1991
2192
|
title
|
|
@@ -1995,8 +2196,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1995
2196
|
edgeId: FT_DROP_ID_BASE + g.id,
|
|
1996
2197
|
kind: "drop",
|
|
1997
2198
|
points: [
|
|
1998
|
-
{ x:
|
|
1999
|
-
{ x:
|
|
2199
|
+
{ x: round5(inst.cx), y: round5(gateTop + glyphH) },
|
|
2200
|
+
{ x: round5(inst.cx), y: round5(by) }
|
|
2000
2201
|
],
|
|
2001
2202
|
instance: null,
|
|
2002
2203
|
title
|
|
@@ -2007,8 +2208,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
2007
2208
|
edgeId: FT_BUS_ID_BASE + g.id,
|
|
2008
2209
|
kind: "bus",
|
|
2009
2210
|
points: [
|
|
2010
|
-
{ x:
|
|
2011
|
-
{ x:
|
|
2211
|
+
{ x: round5(Math.min(...xs)), y: round5(by) },
|
|
2212
|
+
{ x: round5(Math.max(...xs)), y: round5(by) }
|
|
2012
2213
|
],
|
|
2013
2214
|
instance: null,
|
|
2014
2215
|
title
|
|
@@ -2019,8 +2220,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
2019
2220
|
edgeId: FT_RISER_ID_BASE + c.event.id,
|
|
2020
2221
|
kind: "riser",
|
|
2021
2222
|
points: [
|
|
2022
|
-
{ x:
|
|
2023
|
-
{ x:
|
|
2223
|
+
{ x: round5(c.cx), y: round5(by) },
|
|
2224
|
+
{ x: round5(c.cx), y: round5(rowTop[d + 1]) }
|
|
2024
2225
|
],
|
|
2025
2226
|
instance: instanceOf(c),
|
|
2026
2227
|
title
|
|
@@ -2037,42 +2238,42 @@ var GLYPH_STROKE2 = "#52525b";
|
|
|
2037
2238
|
var LABEL_FILL2 = "#3f3f46";
|
|
2038
2239
|
var EDGE_INK3 = "#71717a";
|
|
2039
2240
|
var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="2"`;
|
|
2040
|
-
var
|
|
2241
|
+
var round6 = (n) => Math.round(n * 100) / 100;
|
|
2041
2242
|
function eventGlyph(n) {
|
|
2042
2243
|
const cx = n.cx;
|
|
2043
2244
|
const top = n.top;
|
|
2044
2245
|
if (n.kind === "intermediate") {
|
|
2045
|
-
return `<rect x="${
|
|
2246
|
+
return `<rect x="${round6(cx - n.nodeW / 2)}" y="${top}" width="${n.nodeW}" height="${n.nodeH}" rx="2" ${GLYPH_ATTRS}/>`;
|
|
2046
2247
|
}
|
|
2047
2248
|
if (n.kind === "basic") {
|
|
2048
|
-
return `<circle cx="${cx}" cy="${
|
|
2249
|
+
return `<circle cx="${cx}" cy="${round6(top + 22)}" r="22" ${GLYPH_ATTRS}/>`;
|
|
2049
2250
|
}
|
|
2050
2251
|
if (n.kind === "undeveloped") {
|
|
2051
|
-
const pts2 = `${cx},${top} ${
|
|
2252
|
+
const pts2 = `${cx},${top} ${round6(cx + 24)},${round6(top + 24)} ${cx},${round6(top + 48)} ${round6(cx - 24)},${round6(top + 24)}`;
|
|
2052
2253
|
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
2053
2254
|
}
|
|
2054
2255
|
if (n.kind === "house") {
|
|
2055
|
-
const yB =
|
|
2056
|
-
const eave =
|
|
2057
|
-
const pts2 = `${
|
|
2256
|
+
const yB = round6(top + 40);
|
|
2257
|
+
const eave = round6(top + 16);
|
|
2258
|
+
const pts2 = `${round6(cx - 22)},${yB} ${round6(cx - 22)},${eave} ${cx},${top} ${round6(cx + 22)},${eave} ${round6(cx + 22)},${yB}`;
|
|
2058
2259
|
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
2059
2260
|
}
|
|
2060
2261
|
if (n.kind === "conditioning") {
|
|
2061
|
-
return `<ellipse cx="${cx}" cy="${
|
|
2262
|
+
return `<ellipse cx="${cx}" cy="${round6(top + 16)}" rx="${round6(n.glyphW / 2)}" ry="16" ${GLYPH_ATTRS}/>`;
|
|
2062
2263
|
}
|
|
2063
|
-
const pts = `${cx},${top} ${
|
|
2264
|
+
const pts = `${cx},${top} ${round6(cx + 22)},${round6(top + 35)} ${round6(cx - 22)},${round6(top + 35)}`;
|
|
2064
2265
|
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2065
2266
|
}
|
|
2066
2267
|
function nodeSvg(n) {
|
|
2067
2268
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`, eventGlyph(n)];
|
|
2068
2269
|
if (n.code !== null && n.kind !== "intermediate") {
|
|
2069
2270
|
pieces.push(
|
|
2070
|
-
`<text x="${n.cx}" y="${
|
|
2271
|
+
`<text x="${n.cx}" y="${round6(n.top + n.glyphH / 2 + CODE_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${CODE_FONT}" fill="${LABEL_FILL2}">${xmlEscape(n.code)}</text>`
|
|
2071
2272
|
);
|
|
2072
2273
|
}
|
|
2073
2274
|
if (n.labelLines.length > 0) {
|
|
2074
|
-
const firstBaseline = n.labelTop === null ?
|
|
2075
|
-
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${
|
|
2275
|
+
const firstBaseline = n.labelTop === null ? round6(n.top + 19) : round6(n.labelTop + 10);
|
|
2276
|
+
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round6(firstBaseline + i * FT_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
|
|
2076
2277
|
pieces.push(
|
|
2077
2278
|
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${FT_LABEL_FONT}" fill="${LABEL_FILL2}">${tspans}</text>`
|
|
2078
2279
|
);
|
|
@@ -2081,28 +2282,28 @@ function nodeSvg(n) {
|
|
|
2081
2282
|
return `<g data-node-id="e${n.eventId}"${instance}>${pieces.join("")}</g>`;
|
|
2082
2283
|
}
|
|
2083
2284
|
function orBodyPath(cx, top, yB) {
|
|
2084
|
-
return `M ${
|
|
2285
|
+
return `M ${round6(cx - 22)} ${yB} Q ${cx} ${round6(yB - 14)} ${round6(cx + 22)} ${yB} Q ${round6(cx + 22)} ${round6(top + 14)} ${cx} ${top} Q ${round6(cx - 22)} ${round6(top + 14)} ${round6(cx - 22)} ${yB} Z`;
|
|
2085
2286
|
}
|
|
2086
2287
|
function gateGlyph(g) {
|
|
2087
2288
|
const cx = g.cx;
|
|
2088
2289
|
const top = g.top;
|
|
2089
|
-
const yB =
|
|
2290
|
+
const yB = round6(top + 36);
|
|
2090
2291
|
if (g.type === "and") {
|
|
2091
|
-
const d = `M ${
|
|
2292
|
+
const d = `M ${round6(cx - 22)} ${yB} L ${round6(cx - 22)} ${round6(yB - 14)} A 22 22 0 0 1 ${round6(cx + 22)} ${round6(yB - 14)} L ${round6(cx + 22)} ${yB} Z`;
|
|
2092
2293
|
return `<path d="${d}" ${GLYPH_ATTRS}/>`;
|
|
2093
2294
|
}
|
|
2094
2295
|
if (g.type === "or") {
|
|
2095
2296
|
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/>`;
|
|
2096
2297
|
}
|
|
2097
2298
|
if (g.type === "xor") {
|
|
2098
|
-
const arc = `M ${
|
|
2299
|
+
const arc = `M ${round6(cx - 22)} ${round6(yB + 5)} Q ${cx} ${round6(yB - 9)} ${round6(cx + 22)} ${round6(yB + 5)}`;
|
|
2099
2300
|
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><path d="${arc}" ${GLYPH_ATTRS}/>`;
|
|
2100
2301
|
}
|
|
2101
2302
|
if (g.type === "inhibit") {
|
|
2102
|
-
const pts = `${cx},${top} ${
|
|
2303
|
+
const pts = `${cx},${top} ${round6(cx + 18)},${round6(top + 12)} ${round6(cx + 18)},${round6(top + 34)} ${cx},${round6(top + 46)} ${round6(cx - 18)},${round6(top + 34)} ${round6(cx - 18)},${round6(top + 12)}`;
|
|
2103
2304
|
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2104
2305
|
}
|
|
2105
|
-
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><text x="${cx}" y="${
|
|
2306
|
+
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><text x="${cx}" y="${round6(yB - 10)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL2}">${xmlEscape(g.voteText ?? "")}</text>`;
|
|
2106
2307
|
}
|
|
2107
2308
|
function gateSvg(g) {
|
|
2108
2309
|
return `<g data-node-id="g${g.gateId}"><title>${xmlEscape(g.title)}</title>${gateGlyph(g)}</g>`;
|
|
@@ -2115,34 +2316,34 @@ function elementSvg2(el) {
|
|
|
2115
2316
|
}
|
|
2116
2317
|
var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="1.5"`;
|
|
2117
2318
|
function miniEventSwatch(kind, x, y) {
|
|
2118
|
-
const cx =
|
|
2319
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
2119
2320
|
if (kind === "intermediate") {
|
|
2120
|
-
return `<rect x="${
|
|
2321
|
+
return `<rect x="${round6(cx - 7)}" y="${round6(y - 4.5)}" width="14" height="9" rx="1" ${MINI_ATTRS}/>`;
|
|
2121
2322
|
}
|
|
2122
2323
|
if (kind === "basic") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/>`;
|
|
2123
2324
|
if (kind === "undeveloped") {
|
|
2124
|
-
return `<polygon points="${cx},${
|
|
2325
|
+
return `<polygon points="${cx},${round6(y - 7)} ${round6(cx + 7)},${y} ${cx},${round6(y + 7)} ${round6(cx - 7)},${y}" ${MINI_ATTRS}/>`;
|
|
2125
2326
|
}
|
|
2126
2327
|
if (kind === "house") {
|
|
2127
|
-
return `<polygon points="${
|
|
2328
|
+
return `<polygon points="${round6(cx - 6)},${round6(y + 5.5)} ${round6(cx - 6)},${round6(y - 1)} ${cx},${round6(y - 5.5)} ${round6(cx + 6)},${round6(y - 1)} ${round6(cx + 6)},${round6(y + 5.5)}" ${MINI_ATTRS}/>`;
|
|
2128
2329
|
}
|
|
2129
2330
|
if (kind === "conditioning") return `<ellipse cx="${cx}" cy="${y}" rx="9" ry="5.5" ${MINI_ATTRS}/>`;
|
|
2130
|
-
return `<polygon points="${cx},${
|
|
2331
|
+
return `<polygon points="${cx},${round6(y - 5)} ${round6(cx + 6)},${round6(y + 5)} ${round6(cx - 6)},${round6(y + 5)}" ${MINI_ATTRS}/>`;
|
|
2131
2332
|
}
|
|
2132
2333
|
function miniOrPath(cx, y) {
|
|
2133
|
-
return `M ${
|
|
2334
|
+
return `M ${round6(cx - 7)} ${round6(y + 5.5)} Q ${cx} ${round6(y + 1)} ${round6(cx + 7)} ${round6(y + 5.5)} Q ${round6(cx + 7)} ${round6(y - 1.5)} ${cx} ${round6(y - 5.5)} Q ${round6(cx - 7)} ${round6(y - 1.5)} ${round6(cx - 7)} ${round6(y + 5.5)} Z`;
|
|
2134
2335
|
}
|
|
2135
2336
|
function miniGateSwatch(type, x, y) {
|
|
2136
|
-
const cx =
|
|
2337
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
2137
2338
|
if (type === "and") {
|
|
2138
|
-
const d = `M ${
|
|
2339
|
+
const d = `M ${round6(cx - 7)} ${round6(y + 5.5)} L ${round6(cx - 7)} ${round6(y + 1.5)} A 7 7 0 0 1 ${round6(cx + 7)} ${round6(y + 1.5)} L ${round6(cx + 7)} ${round6(y + 5.5)} Z`;
|
|
2139
2340
|
return `<path d="${d}" ${MINI_ATTRS}/>`;
|
|
2140
2341
|
}
|
|
2141
2342
|
if (type === "xor") {
|
|
2142
|
-
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/><path d="M ${
|
|
2343
|
+
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/><path d="M ${round6(cx - 7)} ${round6(y + 7.5)} Q ${cx} ${round6(y + 3)} ${round6(cx + 7)} ${round6(y + 7.5)}" ${MINI_ATTRS}/>`;
|
|
2143
2344
|
}
|
|
2144
2345
|
if (type === "inhibit") {
|
|
2145
|
-
return `<polygon points="${cx},${
|
|
2346
|
+
return `<polygon points="${cx},${round6(y - 5.5)} ${round6(cx + 4.5)},${round6(y - 2.5)} ${round6(cx + 4.5)},${round6(y + 2.5)} ${cx},${round6(y + 5.5)} ${round6(cx - 4.5)},${round6(y + 2.5)} ${round6(cx - 4.5)},${round6(y - 2.5)}" ${MINI_ATTRS}/>`;
|
|
2146
2347
|
}
|
|
2147
2348
|
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/>`;
|
|
2148
2349
|
}
|
|
@@ -2271,7 +2472,7 @@ var TWIG_W = 1.5;
|
|
|
2271
2472
|
var TWIG_OP = 0.75;
|
|
2272
2473
|
var SUB_W = 1.2;
|
|
2273
2474
|
var SUB_OP = 0.7;
|
|
2274
|
-
var
|
|
2475
|
+
var round7 = (n) => Math.round(n * 100) / 100;
|
|
2275
2476
|
function arrowHead2(tipX, tipY, ux, uy, opacity) {
|
|
2276
2477
|
const LEN = 9;
|
|
2277
2478
|
const HALF_W = 4.5;
|
|
@@ -2280,14 +2481,14 @@ function arrowHead2(tipX, tipY, ux, uy, opacity) {
|
|
|
2280
2481
|
const px = -uy;
|
|
2281
2482
|
const py = ux;
|
|
2282
2483
|
const points = [
|
|
2283
|
-
`${
|
|
2284
|
-
`${
|
|
2285
|
-
`${
|
|
2484
|
+
`${round7(tipX)},${round7(tipY)}`,
|
|
2485
|
+
`${round7(bx + px * HALF_W)},${round7(by + py * HALF_W)}`,
|
|
2486
|
+
`${round7(bx - px * HALF_W)},${round7(by - py * HALF_W)}`
|
|
2286
2487
|
].join(" ");
|
|
2287
2488
|
return `<polygon points="${points}" fill="${EDGE_INK4}" fill-opacity="${opacity}"/>`;
|
|
2288
2489
|
}
|
|
2289
2490
|
function lineEl(x1, y1, x2, y2, w, op) {
|
|
2290
|
-
return `<line x1="${
|
|
2491
|
+
return `<line x1="${round7(x1)}" y1="${round7(y1)}" x2="${round7(x2)}" y2="${round7(y2)}" stroke="${EDGE_INK4}" stroke-width="${w}" stroke-opacity="${op}"/>`;
|
|
2291
2492
|
}
|
|
2292
2493
|
function fishboneSvg(input, opts = {}) {
|
|
2293
2494
|
validateIds(input);
|
|
@@ -2368,7 +2569,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2368
2569
|
{ length: n },
|
|
2369
2570
|
(_, k) => up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH
|
|
2370
2571
|
);
|
|
2371
|
-
const textBlock = (anchor, x, ys, lines) => `<text text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL3}">` + lines.map((line, i) => `<tspan x="${
|
|
2572
|
+
const textBlock = (anchor, x, ys, lines) => `<text text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL3}">` + lines.map((line, i) => `<tspan x="${round7(x)}" y="${round7(ys[i])}">${xmlEscape(line)}</tspan>`).join("") + `</text>`;
|
|
2372
2573
|
const parts = [];
|
|
2373
2574
|
{
|
|
2374
2575
|
const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];
|
|
@@ -2383,7 +2584,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2383
2584
|
if (arrows) body.push(arrowHead2(ax, spineY, COS60, -sgn * SIN60, BONE_OP));
|
|
2384
2585
|
const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;
|
|
2385
2586
|
body.push(
|
|
2386
|
-
`<rect x="${
|
|
2587
|
+
`<rect x="${round7(tipX - bone.boxW / 2)}" y="${round7(boxTop)}" width="${round7(bone.boxW)}" height="${round7(bone.boxH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="1.5"/>`
|
|
2387
2588
|
);
|
|
2388
2589
|
body.push(textBlock("middle", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));
|
|
2389
2590
|
parts.push(
|
|
@@ -2413,7 +2614,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2413
2614
|
{
|
|
2414
2615
|
const x = headLeft + dx;
|
|
2415
2616
|
parts.push(
|
|
2416
|
-
`<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${
|
|
2617
|
+
`<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${round7(x)}" y="${round7(spineY - headH / 2)}" width="${round7(headW)}" height="${round7(headH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="2"/>` + textBlock("middle", x + headW / 2, centeredYs(spineY, effLines.length), effLines) + `</g>`
|
|
2417
2618
|
);
|
|
2418
2619
|
}
|
|
2419
2620
|
const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));
|
|
@@ -2627,13 +2828,13 @@ function pedigreeIssues(input) {
|
|
|
2627
2828
|
);
|
|
2628
2829
|
}
|
|
2629
2830
|
}
|
|
2630
|
-
const
|
|
2831
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
2631
2832
|
"duplicate-id",
|
|
2632
2833
|
"unknown-partner",
|
|
2633
2834
|
"unknown-sibship-mating",
|
|
2634
2835
|
"unknown-child"
|
|
2635
2836
|
]);
|
|
2636
|
-
if (!issues.some((i) =>
|
|
2837
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
2637
2838
|
for (const s of sibships) {
|
|
2638
2839
|
const mating = matingById.get(s.matingId);
|
|
2639
2840
|
if (mating === void 0) continue;
|
|
@@ -2681,7 +2882,7 @@ var PED_DESCENT_ID_BASE = 2e6;
|
|
|
2681
2882
|
var PED_SIBBAR_ID_BASE = 3e6;
|
|
2682
2883
|
var PED_RISER_ID_BASE = 4e6;
|
|
2683
2884
|
var PED_TWINBAR_ID_BASE = 5e6;
|
|
2684
|
-
var
|
|
2885
|
+
var round8 = (n) => Math.round(n * 100) / 100;
|
|
2685
2886
|
function shapeForSex2(sex) {
|
|
2686
2887
|
if (sex === "male") return "square";
|
|
2687
2888
|
if (sex === "female") return "circle";
|
|
@@ -2920,8 +3121,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2920
3121
|
rowHeight[row] = Math.max(rowHeight[row], h);
|
|
2921
3122
|
}
|
|
2922
3123
|
const rowTop = [PADDING5];
|
|
2923
|
-
for (let
|
|
2924
|
-
rowTop.push(rowTop[
|
|
3124
|
+
for (let r2 = 0; r2 < rowCount - 1; r2++) {
|
|
3125
|
+
rowTop.push(rowTop[r2] + rowHeight[r2] + CORRIDOR2);
|
|
2925
3126
|
}
|
|
2926
3127
|
const glyphCyOfRow = (row) => rowTop[row] + PED_GLYPH / 2;
|
|
2927
3128
|
const addressByIndividual = /* @__PURE__ */ new Map();
|
|
@@ -2947,8 +3148,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2947
3148
|
nodes.push({
|
|
2948
3149
|
individualId: ind.id,
|
|
2949
3150
|
shape: shapeForSex2(ind.sex),
|
|
2950
|
-
cx:
|
|
2951
|
-
cy:
|
|
3151
|
+
cx: round8(placed.cx),
|
|
3152
|
+
cy: round8(cy),
|
|
2952
3153
|
size: PED_GLYPH,
|
|
2953
3154
|
deceased: ind.deceased,
|
|
2954
3155
|
carrier: ind.carrier,
|
|
@@ -2958,7 +3159,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2958
3159
|
labelLines: lines,
|
|
2959
3160
|
// Labels sit BELOW the row's serial-union dip band (rowDipBand) so a hub's bridge dips
|
|
2960
3161
|
// never cross a label box.
|
|
2961
|
-
labelTop:
|
|
3162
|
+
labelTop: round8(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
|
|
2962
3163
|
addressLabel: address,
|
|
2963
3164
|
title: individualTitle(ind, address, conditionLabelById, titleLabels)
|
|
2964
3165
|
});
|
|
@@ -3001,23 +3202,23 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3001
3202
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3002
3203
|
kind: "mating-elbow",
|
|
3003
3204
|
points: [
|
|
3004
|
-
{ x:
|
|
3005
|
-
{ x:
|
|
3006
|
-
{ x:
|
|
3007
|
-
{ x:
|
|
3205
|
+
{ x: round8(hubStubX), y: round8(glyphBottom) },
|
|
3206
|
+
{ x: round8(hubStubX), y: round8(dipY) },
|
|
3207
|
+
{ x: round8(spouseStubX), y: round8(dipY) },
|
|
3208
|
+
{ x: round8(spouseStubX), y: round8(glyphBottom) }
|
|
3008
3209
|
],
|
|
3009
3210
|
consanguineous: m.consanguineous,
|
|
3010
3211
|
title
|
|
3011
3212
|
});
|
|
3012
|
-
matingMidpoint.set(m.id, { x:
|
|
3213
|
+
matingMidpoint.set(m.id, { x: round8((hubStubX + spouseStubX) / 2), y: round8(dipY) });
|
|
3013
3214
|
} else if (ay === by) {
|
|
3014
3215
|
const y = ay;
|
|
3015
3216
|
elements.push({
|
|
3016
3217
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3017
3218
|
kind: "mating",
|
|
3018
3219
|
points: [
|
|
3019
|
-
{ x:
|
|
3020
|
-
{ x:
|
|
3220
|
+
{ x: round8(lx), y: round8(y) },
|
|
3221
|
+
{ x: round8(rx), y: round8(y) }
|
|
3021
3222
|
],
|
|
3022
3223
|
consanguineous: m.consanguineous,
|
|
3023
3224
|
title
|
|
@@ -3027,7 +3228,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3027
3228
|
const channelRight = cxOf(rightId) - nodeHalfWidth(individualById.get(rightId));
|
|
3028
3229
|
const barCenter = matingBarCenter.get(m.id);
|
|
3029
3230
|
const originX = barCenter !== void 0 && barCenter >= channelLeft && barCenter <= channelRight ? barCenter : midX;
|
|
3030
|
-
matingMidpoint.set(m.id, { x:
|
|
3231
|
+
matingMidpoint.set(m.id, { x: round8(originX), y: round8(y) });
|
|
3031
3232
|
} else {
|
|
3032
3233
|
const upId = ay <= by ? aId : bId;
|
|
3033
3234
|
const downId = ay <= by ? bId : aId;
|
|
@@ -3046,32 +3247,32 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3046
3247
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3047
3248
|
kind: "mating-elbow",
|
|
3048
3249
|
points: [
|
|
3049
|
-
{ x:
|
|
3050
|
-
{ x:
|
|
3051
|
-
{ x:
|
|
3052
|
-
{ x:
|
|
3053
|
-
{ x:
|
|
3054
|
-
{ x:
|
|
3250
|
+
{ x: round8(upX + (downRight ? -GLYPH_HALF : GLYPH_HALF)), y: round8(upY) },
|
|
3251
|
+
{ x: round8(upExitX), y: round8(upY) },
|
|
3252
|
+
{ x: round8(upExitX), y: round8(laneY) },
|
|
3253
|
+
{ x: round8(downFarX), y: round8(laneY) },
|
|
3254
|
+
{ x: round8(downFarX), y: round8(downY) },
|
|
3255
|
+
{ x: round8(downSideX), y: round8(downY) }
|
|
3055
3256
|
],
|
|
3056
3257
|
consanguineous: m.consanguineous,
|
|
3057
3258
|
title
|
|
3058
3259
|
});
|
|
3059
|
-
matingMidpoint.set(m.id, { x:
|
|
3260
|
+
matingMidpoint.set(m.id, { x: round8(downFarX), y: round8(downY) });
|
|
3060
3261
|
} else {
|
|
3061
3262
|
const elbowX = (upX + downX) / 2;
|
|
3062
3263
|
elements.push({
|
|
3063
3264
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3064
3265
|
kind: "mating-elbow",
|
|
3065
3266
|
points: [
|
|
3066
|
-
{ x:
|
|
3067
|
-
{ x:
|
|
3068
|
-
{ x:
|
|
3069
|
-
{ x:
|
|
3267
|
+
{ x: round8(upX + (downX >= upX ? GLYPH_HALF : -GLYPH_HALF)), y: round8(upY) },
|
|
3268
|
+
{ x: round8(elbowX), y: round8(upY) },
|
|
3269
|
+
{ x: round8(elbowX), y: round8(downY) },
|
|
3270
|
+
{ x: round8(downX + (upX >= downX ? GLYPH_HALF : -GLYPH_HALF)), y: round8(downY) }
|
|
3070
3271
|
],
|
|
3071
3272
|
consanguineous: m.consanguineous,
|
|
3072
3273
|
title
|
|
3073
3274
|
});
|
|
3074
|
-
matingMidpoint.set(m.id, { x:
|
|
3275
|
+
matingMidpoint.set(m.id, { x: round8(elbowX), y: round8(downY) });
|
|
3075
3276
|
}
|
|
3076
3277
|
}
|
|
3077
3278
|
}
|
|
@@ -3093,13 +3294,13 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3093
3294
|
const barCenterX = (barLeft + barRight) / 2;
|
|
3094
3295
|
const bendY = barY - 2 - lane % 3 * 3;
|
|
3095
3296
|
const descentPoints = Math.abs(mid.x - barCenterX) < 0.01 ? [
|
|
3096
|
-
{ x:
|
|
3097
|
-
{ x:
|
|
3297
|
+
{ x: round8(mid.x), y: round8(mid.y) },
|
|
3298
|
+
{ x: round8(mid.x), y: round8(barY) }
|
|
3098
3299
|
] : [
|
|
3099
|
-
{ x:
|
|
3100
|
-
{ x:
|
|
3101
|
-
{ x:
|
|
3102
|
-
{ x:
|
|
3300
|
+
{ x: round8(mid.x), y: round8(mid.y) },
|
|
3301
|
+
{ x: round8(mid.x), y: round8(bendY) },
|
|
3302
|
+
{ x: round8(barCenterX), y: round8(bendY) },
|
|
3303
|
+
{ x: round8(barCenterX), y: round8(barY) }
|
|
3103
3304
|
];
|
|
3104
3305
|
elements.push({
|
|
3105
3306
|
edgeId: PED_DESCENT_ID_BASE + s.id,
|
|
@@ -3114,8 +3315,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3114
3315
|
edgeId: PED_SIBBAR_ID_BASE + s.id,
|
|
3115
3316
|
kind: "sibship-bar",
|
|
3116
3317
|
points: [
|
|
3117
|
-
{ x:
|
|
3118
|
-
{ x:
|
|
3318
|
+
{ x: round8(barLeft), y: round8(barY) },
|
|
3319
|
+
{ x: round8(barRight), y: round8(barY) }
|
|
3119
3320
|
],
|
|
3120
3321
|
consanguineous: false,
|
|
3121
3322
|
title: titleLabels.sibship
|
|
@@ -3136,8 +3337,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3136
3337
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
3137
3338
|
kind: "riser",
|
|
3138
3339
|
points: [
|
|
3139
|
-
{ x:
|
|
3140
|
-
{ x:
|
|
3340
|
+
{ x: round8(cx), y: round8(barY) },
|
|
3341
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3141
3342
|
],
|
|
3142
3343
|
consanguineous: false,
|
|
3143
3344
|
title: titleLabels.sibship
|
|
@@ -3151,15 +3352,15 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3151
3352
|
if (!emittedTwinOrdinals.has(tg.ordinal)) {
|
|
3152
3353
|
emittedTwinOrdinals.add(tg.ordinal);
|
|
3153
3354
|
if (tg.zygosity === "unknown") {
|
|
3154
|
-
unknownTwinJunctions.push({ x:
|
|
3355
|
+
unknownTwinJunctions.push({ x: round8(junctionX), y: round8(junctionY) });
|
|
3155
3356
|
}
|
|
3156
3357
|
elements.push({
|
|
3157
3358
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
3158
3359
|
// anchored on the first member for a stable id
|
|
3159
3360
|
kind: "riser",
|
|
3160
3361
|
points: [
|
|
3161
|
-
{ x:
|
|
3162
|
-
{ x:
|
|
3362
|
+
{ x: round8(junctionX), y: round8(barY) },
|
|
3363
|
+
{ x: round8(junctionX), y: round8(junctionY) }
|
|
3163
3364
|
],
|
|
3164
3365
|
consanguineous: false,
|
|
3165
3366
|
title: titleLabels.twins[tg.zygosity]
|
|
@@ -3170,8 +3371,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3170
3371
|
edgeId: PED_TWINBAR_ID_BASE + s.id * 100 + tg.ordinal,
|
|
3171
3372
|
kind: "twin-bar",
|
|
3172
3373
|
points: [
|
|
3173
|
-
{ x:
|
|
3174
|
-
{ x:
|
|
3374
|
+
{ x: round8(Math.min(...memberXs)), y: round8(tieY) },
|
|
3375
|
+
{ x: round8(Math.max(...memberXs)), y: round8(tieY) }
|
|
3175
3376
|
],
|
|
3176
3377
|
consanguineous: false,
|
|
3177
3378
|
title: titleLabels.twins.mz
|
|
@@ -3179,12 +3380,12 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3179
3380
|
}
|
|
3180
3381
|
}
|
|
3181
3382
|
const horizontalThenDown = Math.abs(cx - junctionX) < 0.01 ? [
|
|
3182
|
-
{ x:
|
|
3183
|
-
{ x:
|
|
3383
|
+
{ x: round8(cx), y: round8(junctionY) },
|
|
3384
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3184
3385
|
] : [
|
|
3185
|
-
{ x:
|
|
3186
|
-
{ x:
|
|
3187
|
-
{ x:
|
|
3386
|
+
{ x: round8(junctionX), y: round8(junctionY) },
|
|
3387
|
+
{ x: round8(cx), y: round8(junctionY) },
|
|
3388
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3188
3389
|
];
|
|
3189
3390
|
elements.push({
|
|
3190
3391
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
@@ -3212,7 +3413,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3212
3413
|
}
|
|
3213
3414
|
const generations = [];
|
|
3214
3415
|
for (let row = 0; row < rowCount; row++) {
|
|
3215
|
-
generations.push({ roman: romanNumeral(row + 1), y:
|
|
3416
|
+
generations.push({ roman: romanNumeral(row + 1), y: round8(glyphCyOfRow(row)) });
|
|
3216
3417
|
}
|
|
3217
3418
|
const ZYGOSITY_ORDER = ["mz", "dz", "unknown"];
|
|
3218
3419
|
const twinZygositiesUsed = ZYGOSITY_ORDER.filter((z) => twinZygositiesUsedSet.has(z));
|
|
@@ -3236,15 +3437,15 @@ var EDGE_INK5 = "#71717a";
|
|
|
3236
3437
|
var GLYPH_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="2"`;
|
|
3237
3438
|
var CONSANG_GAP = 3;
|
|
3238
3439
|
var ZYGOSITIES = ["mz", "dz", "unknown"];
|
|
3239
|
-
var
|
|
3440
|
+
var round9 = (n) => Math.round(n * 100) / 100;
|
|
3240
3441
|
function glyphOutline(shape, cx, cy, half) {
|
|
3241
3442
|
if (shape === "square") {
|
|
3242
|
-
return `<rect x="${
|
|
3443
|
+
return `<rect x="${round9(cx - half)}" y="${round9(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS2}/>`;
|
|
3243
3444
|
}
|
|
3244
3445
|
if (shape === "circle") {
|
|
3245
3446
|
return `<circle cx="${cx}" cy="${cy}" r="${half}" ${GLYPH_ATTRS2}/>`;
|
|
3246
3447
|
}
|
|
3247
|
-
return `<polygon points="${cx},${
|
|
3448
|
+
return `<polygon points="${cx},${round9(cy - half)} ${round9(cx + half)},${cy} ${cx},${round9(cy + half)} ${round9(cx - half)},${cy}" ${GLYPH_ATTRS2}/>`;
|
|
3248
3449
|
}
|
|
3249
3450
|
function glyphVerticalExtentAt(shape, cy, half, dx) {
|
|
3250
3451
|
const ax = Math.min(Math.abs(dx), half);
|
|
@@ -3270,8 +3471,8 @@ function fillPartitions(n, half, inkByCondition) {
|
|
|
3270
3471
|
for (let s = 0; s <= SAMPLES; s++) {
|
|
3271
3472
|
const x = left + sliceW * s / SAMPLES;
|
|
3272
3473
|
const [yt, yb] = glyphVerticalExtentAt(n.shape, cy, half, x - cx);
|
|
3273
|
-
top.push(`${
|
|
3274
|
-
bottom.push(`${
|
|
3474
|
+
top.push(`${round9(x)},${round9(yt)}`);
|
|
3475
|
+
bottom.push(`${round9(x)},${round9(yb)}`);
|
|
3275
3476
|
}
|
|
3276
3477
|
const pts = [...top, ...bottom.reverse()].join(" ");
|
|
3277
3478
|
const ink = inkByCondition.get(id) ?? GLYPH_STROKE3;
|
|
@@ -3285,24 +3486,24 @@ function carrierDot(n) {
|
|
|
3285
3486
|
function deceasedSlash(n, half) {
|
|
3286
3487
|
if (!n.deceased) return "";
|
|
3287
3488
|
const ext = half + 4;
|
|
3288
|
-
return `<line x1="${
|
|
3489
|
+
return `<line x1="${round9(n.cx - ext)}" y1="${round9(n.cy - ext)}" x2="${round9(n.cx + ext)}" y2="${round9(n.cy + ext)}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
|
|
3289
3490
|
}
|
|
3290
3491
|
function probandArrow(n, half) {
|
|
3291
3492
|
if (n.role === null) return "";
|
|
3292
|
-
const tipX =
|
|
3293
|
-
const tipY =
|
|
3294
|
-
const tailX =
|
|
3295
|
-
const tailY =
|
|
3493
|
+
const tipX = round9(n.cx - half);
|
|
3494
|
+
const tipY = round9(n.cy + half);
|
|
3495
|
+
const tailX = round9(tipX - 16);
|
|
3496
|
+
const tailY = round9(tipY + 16);
|
|
3296
3497
|
const filled = n.role === "proband";
|
|
3297
3498
|
const fill = filled ? GLYPH_STROKE3 : "transparent";
|
|
3298
3499
|
const shaft = `<line x1="${tailX}" y1="${tailY}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
|
|
3299
|
-
const head = `<polygon points="${tipX},${tipY} ${
|
|
3500
|
+
const head = `<polygon points="${tipX},${tipY} ${round9(tipX - 8)},${round9(tipY + 2)} ${round9(tipX - 2)},${round9(tipY + 8)}" fill="${fill}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3300
3501
|
return shaft + head;
|
|
3301
3502
|
}
|
|
3302
3503
|
function stillbirthMark(n, half) {
|
|
3303
3504
|
if (!n.stillbirth) return "";
|
|
3304
|
-
const y =
|
|
3305
|
-
return `<text x="${
|
|
3505
|
+
const y = round9(n.cy + half + PED_ADDRESS_FONT);
|
|
3506
|
+
return `<text x="${round9(n.cx - half - 2)}" y="${y}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" font-weight="bold" fill="${LABEL_FILL4}">SB</text>`;
|
|
3306
3507
|
}
|
|
3307
3508
|
function nodeSvg2(n, inkByCondition) {
|
|
3308
3509
|
const half = n.size / 2;
|
|
@@ -3316,13 +3517,13 @@ function nodeSvg2(n, inkByCondition) {
|
|
|
3316
3517
|
probandArrow(n, half),
|
|
3317
3518
|
stillbirthMark(n, half)
|
|
3318
3519
|
];
|
|
3319
|
-
const addressY =
|
|
3520
|
+
const addressY = round9(n.labelTop + PED_ADDRESS_FONT);
|
|
3320
3521
|
pieces.push(
|
|
3321
3522
|
`<text x="${n.cx}" y="${addressY}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" fill="${LABEL_FILL4}">${xmlEscape(n.addressLabel)}</text>`
|
|
3322
3523
|
);
|
|
3323
3524
|
if (n.labelLines.length > 0) {
|
|
3324
|
-
const firstBaseline =
|
|
3325
|
-
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${
|
|
3525
|
+
const firstBaseline = round9(n.labelTop + PED_LABEL_LINE_H + 10);
|
|
3526
|
+
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round9(firstBaseline + i * PED_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
|
|
3326
3527
|
pieces.push(
|
|
3327
3528
|
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" fill="${LABEL_FILL4}">${tspans}</text>`
|
|
3328
3529
|
);
|
|
@@ -3337,7 +3538,7 @@ function elementSvg3(el) {
|
|
|
3337
3538
|
const title = `<title>${xmlEscape(el.title)}</title>`;
|
|
3338
3539
|
const stroke = `stroke="${EDGE_INK5}" stroke-width="1.5" stroke-opacity="0.75"`;
|
|
3339
3540
|
const draw = (offsetY) => {
|
|
3340
|
-
const shifted = pts.map((p) => ({ x: p.x, y:
|
|
3541
|
+
const shifted = pts.map((p) => ({ x: p.x, y: round9(p.y + offsetY) }));
|
|
3341
3542
|
if (shifted.length === 2) {
|
|
3342
3543
|
return `<line x1="${shifted[0].x}" y1="${shifted[0].y}" x2="${shifted[1].x}" y2="${shifted[1].y}" ${stroke}/>`;
|
|
3343
3544
|
}
|
|
@@ -3348,51 +3549,51 @@ function elementSvg3(el) {
|
|
|
3348
3549
|
}
|
|
3349
3550
|
function unknownTwinMarks(layout) {
|
|
3350
3551
|
return layout.unknownTwinJunctions.map(
|
|
3351
|
-
(p) => `<text x="${
|
|
3552
|
+
(p) => `<text x="${round9(p.x + 6)}" y="${round9(p.y + 4)}" font-family="${FONT_FAMILY}" font-size="12" font-weight="bold" fill="${LABEL_FILL4}">?</text>`
|
|
3352
3553
|
).join("");
|
|
3353
3554
|
}
|
|
3354
3555
|
var MINI_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="1.5"`;
|
|
3355
3556
|
function miniShapeSwatch(shape, x, y) {
|
|
3356
|
-
const cx =
|
|
3357
|
-
if (shape === "square") return `<rect x="${
|
|
3557
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3558
|
+
if (shape === "square") return `<rect x="${round9(cx - 6)}" y="${round9(y - 6)}" width="12" height="12" ${MINI_ATTRS2}/>`;
|
|
3358
3559
|
if (shape === "circle") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/>`;
|
|
3359
|
-
return `<polygon points="${cx},${
|
|
3560
|
+
return `<polygon points="${cx},${round9(y - 7)} ${round9(cx + 7)},${y} ${cx},${round9(y + 7)} ${round9(cx - 7)},${y}" ${MINI_ATTRS2}/>`;
|
|
3360
3561
|
}
|
|
3361
3562
|
function miniSwatchCircle(filled, ink, x, y) {
|
|
3362
|
-
const cx =
|
|
3563
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3363
3564
|
return `<circle cx="${cx}" cy="${y}" r="6" fill="${filled ? ink : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3364
3565
|
}
|
|
3365
3566
|
function miniCarrierSwatch(x, y) {
|
|
3366
|
-
const cx =
|
|
3567
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3367
3568
|
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><circle cx="${cx}" cy="${y}" r="2" fill="${GLYPH_STROKE3}" stroke="none"/>`;
|
|
3368
3569
|
}
|
|
3369
3570
|
function miniDeceasedSwatch(x, y) {
|
|
3370
|
-
const cx =
|
|
3371
|
-
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><line x1="${
|
|
3571
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3572
|
+
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><line x1="${round9(cx - 7)}" y1="${round9(y - 7)}" x2="${round9(cx + 7)}" y2="${round9(y + 7)}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3372
3573
|
}
|
|
3373
3574
|
function miniArrowSwatch(filled, x, y) {
|
|
3374
|
-
const cx =
|
|
3375
|
-
const tipX =
|
|
3376
|
-
const tipY =
|
|
3377
|
-
return `<line x1="${
|
|
3575
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3576
|
+
const tipX = round9(cx - 2);
|
|
3577
|
+
const tipY = round9(y + 2);
|
|
3578
|
+
return `<line x1="${round9(tipX - 8)}" y1="${round9(tipY + 8)}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/><polygon points="${tipX},${tipY} ${round9(tipX - 5)},${round9(tipY + 1)} ${round9(tipX - 1)},${round9(tipY + 5)}" fill="${filled ? GLYPH_STROKE3 : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1"/>`;
|
|
3378
3579
|
}
|
|
3379
3580
|
function miniConsanguineousSwatch(x, y) {
|
|
3380
|
-
const x1 =
|
|
3381
|
-
const x2 =
|
|
3382
|
-
return `<line x1="${x1}" y1="${
|
|
3581
|
+
const x1 = round9(x + 2);
|
|
3582
|
+
const x2 = round9(x + LEGEND_SWATCH_W - 2);
|
|
3583
|
+
return `<line x1="${x1}" y1="${round9(y - 1.5)}" x2="${x2}" y2="${round9(y - 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${x1}" y1="${round9(y + 1.5)}" x2="${x2}" y2="${round9(y + 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3383
3584
|
}
|
|
3384
3585
|
function miniTwinSwatch(zygosity, x, y) {
|
|
3385
|
-
const cx =
|
|
3386
|
-
const apexY =
|
|
3387
|
-
const baseY =
|
|
3388
|
-
const left =
|
|
3389
|
-
const right =
|
|
3586
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3587
|
+
const apexY = round9(y - 6);
|
|
3588
|
+
const baseY = round9(y + 6);
|
|
3589
|
+
const left = round9(cx - 6);
|
|
3590
|
+
const right = round9(cx + 6);
|
|
3390
3591
|
const stub = `<line x1="${cx}" y1="${apexY}" x2="${cx}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${right}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${left}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${right}" y1="${y}" x2="${right}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3391
3592
|
if (zygosity === "mz") {
|
|
3392
|
-
return stub + `<line x1="${left}" y1="${
|
|
3593
|
+
return stub + `<line x1="${left}" y1="${round9(y + 3)}" x2="${right}" y2="${round9(y + 3)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3393
3594
|
}
|
|
3394
3595
|
if (zygosity === "unknown") {
|
|
3395
|
-
return stub + `<text x="${
|
|
3596
|
+
return stub + `<text x="${round9(cx + 8)}" y="${round9(y + 3)}" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL4}">?</text>`;
|
|
3396
3597
|
}
|
|
3397
3598
|
return stub;
|
|
3398
3599
|
}
|
|
@@ -3405,7 +3606,7 @@ function pedigreeLayoutSvg(layout, opts = {}) {
|
|
|
3405
3606
|
for (const n of layout.nodes) parts.push(nodeSvg2(n, inkByCondition));
|
|
3406
3607
|
for (const g of layout.generations) {
|
|
3407
3608
|
parts.push(
|
|
3408
|
-
`<text x="${
|
|
3609
|
+
`<text x="${round9(16)}" y="${round9(g.y + PED_LABEL_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" font-weight="bold" fill="${LABEL_FILL4}">${xmlEscape(g.roman)}</text>`
|
|
3409
3610
|
);
|
|
3410
3611
|
}
|
|
3411
3612
|
let width = layout.width;
|
|
@@ -3571,13 +3772,13 @@ function phyloIssues(input) {
|
|
|
3571
3772
|
push("tip-with-children", `node ${n.id} is declared internal but has no children`);
|
|
3572
3773
|
}
|
|
3573
3774
|
}
|
|
3574
|
-
const
|
|
3775
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
3575
3776
|
"duplicate-id",
|
|
3576
3777
|
"unknown-endpoint",
|
|
3577
3778
|
"unknown-root",
|
|
3578
3779
|
"multiple-parents"
|
|
3579
3780
|
]);
|
|
3580
|
-
if (!issues.some((i) =>
|
|
3781
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
3581
3782
|
const childrenOf = /* @__PURE__ */ new Map();
|
|
3582
3783
|
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
3583
3784
|
const arr = childrenOf.get(e.parentId) ?? [];
|
|
@@ -3655,7 +3856,7 @@ var PHYLO_BRANCH_ID_BASE = 2e6;
|
|
|
3655
3856
|
var PHYLO_EXTENSION_ID_BASE = 3e6;
|
|
3656
3857
|
var PHYLO_ROOTSTUB_ID_BASE = 4e6;
|
|
3657
3858
|
var PHYLO_SCALEBAR_ID = 5e6;
|
|
3658
|
-
var
|
|
3859
|
+
var round10 = (n) => Math.round(n * 100) / 100;
|
|
3659
3860
|
function niceScaleStep(maxLen) {
|
|
3660
3861
|
if (!(maxLen > 0)) return 0;
|
|
3661
3862
|
const target = maxLen / 4;
|
|
@@ -3781,8 +3982,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3781
3982
|
edgeId: PHYLO_ROOTSTUB_ID_BASE + root.node.id,
|
|
3782
3983
|
kind: "root-stub",
|
|
3783
3984
|
points: [
|
|
3784
|
-
{ x:
|
|
3785
|
-
{ x:
|
|
3985
|
+
{ x: round10(Math.max(PADDING6 / 2, rootDrawX - ROOT_STUB)), y: round10(root.cy) },
|
|
3986
|
+
{ x: round10(rootDrawX), y: round10(root.cy) }
|
|
3786
3987
|
],
|
|
3787
3988
|
dotted: false,
|
|
3788
3989
|
length: null,
|
|
@@ -3797,11 +3998,11 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3797
3998
|
nodes.push({
|
|
3798
3999
|
nodeId: w.node.id,
|
|
3799
4000
|
isTip: tip,
|
|
3800
|
-
cx:
|
|
3801
|
-
cy:
|
|
4001
|
+
cx: round10(drawX),
|
|
4002
|
+
cy: round10(w.cy),
|
|
3802
4003
|
support,
|
|
3803
4004
|
labelLines: labelLine,
|
|
3804
|
-
labelLeft:
|
|
4005
|
+
labelLeft: round10(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
|
|
3805
4006
|
depth: w.depth,
|
|
3806
4007
|
title: nodeTitle(w.node, tip, isRoot, w.inLength, titleLabels)
|
|
3807
4008
|
});
|
|
@@ -3810,8 +4011,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3810
4011
|
edgeId: PHYLO_EXTENSION_ID_BASE + w.node.id,
|
|
3811
4012
|
kind: "extension",
|
|
3812
4013
|
points: [
|
|
3813
|
-
{ x:
|
|
3814
|
-
{ x:
|
|
4014
|
+
{ x: round10(w.cx), y: round10(w.cy) },
|
|
4015
|
+
{ x: round10(alignX), y: round10(w.cy) }
|
|
3815
4016
|
],
|
|
3816
4017
|
dotted: true,
|
|
3817
4018
|
length: null,
|
|
@@ -3827,8 +4028,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3827
4028
|
edgeId: PHYLO_CLADEBAR_ID_BASE + w.node.id,
|
|
3828
4029
|
kind: "clade-bar",
|
|
3829
4030
|
points: [
|
|
3830
|
-
{ x:
|
|
3831
|
-
{ x:
|
|
4031
|
+
{ x: round10(w.cx), y: round10(barTop) },
|
|
4032
|
+
{ x: round10(w.cx), y: round10(barBottom) }
|
|
3832
4033
|
],
|
|
3833
4034
|
dotted: false,
|
|
3834
4035
|
length: null,
|
|
@@ -3840,8 +4041,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3840
4041
|
edgeId: PHYLO_BRANCH_ID_BASE + c.node.id,
|
|
3841
4042
|
kind: "branch",
|
|
3842
4043
|
points: [
|
|
3843
|
-
{ x:
|
|
3844
|
-
{ x:
|
|
4044
|
+
{ x: round10(w.cx), y: round10(c.cy) },
|
|
4045
|
+
{ x: round10(c.cx), y: round10(c.cy) }
|
|
3845
4046
|
],
|
|
3846
4047
|
dotted: false,
|
|
3847
4048
|
length: c.inLength,
|
|
@@ -3858,17 +4059,17 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3858
4059
|
const barY = treeBottom + SCALEBAR_ZONE / 2;
|
|
3859
4060
|
scaleBar = {
|
|
3860
4061
|
length: stepLen,
|
|
3861
|
-
x:
|
|
3862
|
-
y:
|
|
3863
|
-
pxLength:
|
|
4062
|
+
x: round10(barX),
|
|
4063
|
+
y: round10(barY),
|
|
4064
|
+
pxLength: round10(scaleBarPx),
|
|
3864
4065
|
valueLabel: trimNumber(stepLen)
|
|
3865
4066
|
};
|
|
3866
4067
|
elements.push({
|
|
3867
4068
|
edgeId: PHYLO_SCALEBAR_ID,
|
|
3868
4069
|
kind: "scale-bar",
|
|
3869
4070
|
points: [
|
|
3870
|
-
{ x:
|
|
3871
|
-
{ x:
|
|
4071
|
+
{ x: round10(barX), y: round10(barY) },
|
|
4072
|
+
{ x: round10(barX + scaleBarPx), y: round10(barY) }
|
|
3872
4073
|
],
|
|
3873
4074
|
dotted: false,
|
|
3874
4075
|
length: stepLen,
|
|
@@ -3882,18 +4083,18 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3882
4083
|
var GLYPH_STROKE4 = "#52525b";
|
|
3883
4084
|
var LABEL_FILL5 = "#3f3f46";
|
|
3884
4085
|
var EDGE_INK6 = "#71717a";
|
|
3885
|
-
var
|
|
4086
|
+
var round11 = (n) => Math.round(n * 100) / 100;
|
|
3886
4087
|
function nodeSvg3(n, showSupport) {
|
|
3887
4088
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
3888
4089
|
pieces.push(`<circle cx="${n.cx}" cy="${n.cy}" r="2.5" fill="${GLYPH_STROKE4}"/>`);
|
|
3889
4090
|
if (n.labelLines.length > 0) {
|
|
3890
4091
|
pieces.push(
|
|
3891
|
-
`<text x="${n.labelLeft}" y="${
|
|
4092
|
+
`<text x="${n.labelLeft}" y="${round11(n.cy + PHYLO_LABEL_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_LABEL_FONT}" fill="${LABEL_FILL5}">${xmlEscape(n.labelLines[0])}</text>`
|
|
3892
4093
|
);
|
|
3893
4094
|
}
|
|
3894
4095
|
if (showSupport && !n.isTip && n.support !== null) {
|
|
3895
4096
|
pieces.push(
|
|
3896
|
-
`<text x="${
|
|
4097
|
+
`<text x="${round11(n.cx - 4)}" y="${round11(n.cy - 3)}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(String(n.support))}</text>`
|
|
3897
4098
|
);
|
|
3898
4099
|
}
|
|
3899
4100
|
return `<g data-node-id="n${n.nodeId}">${pieces.join("")}</g>`;
|
|
@@ -3908,18 +4109,18 @@ function elementSvg4(el) {
|
|
|
3908
4109
|
function scaleBarLabelSvg(layout) {
|
|
3909
4110
|
const bar = layout.scaleBar;
|
|
3910
4111
|
if (bar === null) return "";
|
|
3911
|
-
const tick = (x) => `<line x1="${x}" y1="${
|
|
3912
|
-
return tick(bar.x) + tick(
|
|
4112
|
+
const tick = (x) => `<line x1="${x}" y1="${round11(bar.y - 3)}" x2="${x}" y2="${round11(bar.y + 3)}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
4113
|
+
return tick(bar.x) + tick(round11(bar.x + bar.pxLength)) + `<text x="${round11(bar.x + bar.pxLength / 2)}" y="${round11(bar.y - 6)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(bar.valueLabel)}</text>`;
|
|
3913
4114
|
}
|
|
3914
4115
|
function supportSwatch(x, y) {
|
|
3915
|
-
const cx =
|
|
3916
|
-
return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE4}"/><text x="${
|
|
4116
|
+
const cx = round11(x + LEGEND_SWATCH_W / 2);
|
|
4117
|
+
return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE4}"/><text x="${round11(cx + 5)}" y="${round11(y + PHYLO_SUPPORT_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">95</text>`;
|
|
3917
4118
|
}
|
|
3918
4119
|
function scaleSwatch(x, y) {
|
|
3919
|
-
return `<line x1="${
|
|
4120
|
+
return `<line x1="${round11(x)}" y1="${y}" x2="${round11(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
3920
4121
|
}
|
|
3921
4122
|
function alignedTipSwatch(x, y) {
|
|
3922
|
-
return `<line x1="${
|
|
4123
|
+
return `<line x1="${round11(x)}" y1="${y}" x2="${round11(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="2,3"/>`;
|
|
3923
4124
|
}
|
|
3924
4125
|
function phyloLayoutSvg(layout, opts = {}) {
|
|
3925
4126
|
const labels = opts.labels ?? PHYLO_SVG_LABELS_EN;
|
|
@@ -4024,9 +4225,9 @@ function orgChartIssues(input) {
|
|
|
4024
4225
|
}
|
|
4025
4226
|
const reportById = /* @__PURE__ */ new Map();
|
|
4026
4227
|
const dupReportIds = /* @__PURE__ */ new Set();
|
|
4027
|
-
for (const
|
|
4028
|
-
if (reportById.has(
|
|
4029
|
-
else reportById.set(
|
|
4228
|
+
for (const r2 of input.reports) {
|
|
4229
|
+
if (reportById.has(r2.id)) dupReportIds.add(r2.id);
|
|
4230
|
+
else reportById.set(r2.id, r2);
|
|
4030
4231
|
}
|
|
4031
4232
|
for (const id of [...dupReportIds].sort((a, b) => a - b)) {
|
|
4032
4233
|
push("duplicate-id", `duplicate report id ${id}`);
|
|
@@ -4039,32 +4240,32 @@ function orgChartIssues(input) {
|
|
|
4039
4240
|
const assistantReportEdge = /* @__PURE__ */ new Map();
|
|
4040
4241
|
const solidManagedEdges = /* @__PURE__ */ new Map();
|
|
4041
4242
|
const dottedDegree = /* @__PURE__ */ new Map();
|
|
4042
|
-
for (const
|
|
4043
|
-
const hasManager = positionById.has(
|
|
4044
|
-
const hasReport = positionById.has(
|
|
4243
|
+
for (const r2 of reports) {
|
|
4244
|
+
const hasManager = positionById.has(r2.managerId);
|
|
4245
|
+
const hasReport = positionById.has(r2.reportId);
|
|
4045
4246
|
if (!hasManager) {
|
|
4046
|
-
push("unknown-manager", `report ${
|
|
4247
|
+
push("unknown-manager", `report ${r2.id} managerId ${r2.managerId} is not a declared position`);
|
|
4047
4248
|
}
|
|
4048
4249
|
if (!hasReport) {
|
|
4049
|
-
push("unknown-report", `report ${
|
|
4250
|
+
push("unknown-report", `report ${r2.id} reportId ${r2.reportId} is not a declared position`);
|
|
4050
4251
|
}
|
|
4051
|
-
if (
|
|
4052
|
-
push("self-report", `report ${
|
|
4252
|
+
if (r2.managerId === r2.reportId) {
|
|
4253
|
+
push("self-report", `report ${r2.id} has managerId === reportId (${r2.managerId})`);
|
|
4053
4254
|
}
|
|
4054
|
-
if (
|
|
4055
|
-
const arr = solidParents.get(
|
|
4056
|
-
arr.push(
|
|
4057
|
-
solidParents.set(
|
|
4058
|
-
const managed = solidManagedEdges.get(
|
|
4059
|
-
managed.push(
|
|
4060
|
-
solidManagedEdges.set(
|
|
4255
|
+
if (r2.kind !== "dotted") {
|
|
4256
|
+
const arr = solidParents.get(r2.reportId) ?? [];
|
|
4257
|
+
arr.push(r2.id);
|
|
4258
|
+
solidParents.set(r2.reportId, arr);
|
|
4259
|
+
const managed = solidManagedEdges.get(r2.managerId) ?? [];
|
|
4260
|
+
managed.push(r2.id);
|
|
4261
|
+
solidManagedEdges.set(r2.managerId, managed);
|
|
4061
4262
|
}
|
|
4062
|
-
if (
|
|
4063
|
-
assistantReportEdge.set(
|
|
4263
|
+
if (r2.kind === "assistant" && r2.managerId !== r2.reportId && !assistantReportEdge.has(r2.reportId)) {
|
|
4264
|
+
assistantReportEdge.set(r2.reportId, r2.id);
|
|
4064
4265
|
}
|
|
4065
|
-
if (
|
|
4066
|
-
if (positionById.has(
|
|
4067
|
-
if (positionById.has(
|
|
4266
|
+
if (r2.kind === "dotted" && r2.managerId !== r2.reportId) {
|
|
4267
|
+
if (positionById.has(r2.managerId)) dottedDegree.set(r2.managerId, (dottedDegree.get(r2.managerId) ?? 0) + 1);
|
|
4268
|
+
if (positionById.has(r2.reportId)) dottedDegree.set(r2.reportId, (dottedDegree.get(r2.reportId) ?? 0) + 1);
|
|
4068
4269
|
}
|
|
4069
4270
|
}
|
|
4070
4271
|
for (const [reportId, edgeIds] of [...solidParents.entries()].sort((a, b) => a[0] - b[0])) {
|
|
@@ -4095,12 +4296,12 @@ function orgChartIssues(input) {
|
|
|
4095
4296
|
}
|
|
4096
4297
|
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
4097
4298
|
const solidChildren = /* @__PURE__ */ new Map();
|
|
4098
|
-
for (const
|
|
4099
|
-
if (
|
|
4100
|
-
if (
|
|
4101
|
-
const arr = solidChildren.get(
|
|
4102
|
-
arr.push(
|
|
4103
|
-
solidChildren.set(
|
|
4299
|
+
for (const r2 of reports) {
|
|
4300
|
+
if (r2.kind === "dotted") continue;
|
|
4301
|
+
if (r2.managerId === r2.reportId) continue;
|
|
4302
|
+
const arr = solidChildren.get(r2.managerId) ?? [];
|
|
4303
|
+
arr.push(r2.reportId);
|
|
4304
|
+
solidChildren.set(r2.managerId, arr);
|
|
4104
4305
|
}
|
|
4105
4306
|
for (const arr of solidChildren.values()) arr.sort((a, b) => a - b);
|
|
4106
4307
|
const color = /* @__PURE__ */ new Map();
|
|
@@ -4167,26 +4368,9 @@ var ORG_BUS_ID_BASE = 2e6;
|
|
|
4167
4368
|
var ORG_DROP_ID_BASE = 3e6;
|
|
4168
4369
|
var ORG_ASSIST_ID_BASE = 4e6;
|
|
4169
4370
|
var ORG_DOTTED_ID_BASE = 5e6;
|
|
4170
|
-
var
|
|
4171
|
-
var drawnLeftEdge = (cx, boxW) =>
|
|
4172
|
-
var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) +
|
|
4173
|
-
function packSubtree(node, gaps) {
|
|
4174
|
-
const { ownHalfL, ownHalfR, children } = node;
|
|
4175
|
-
if (children.length === 0) {
|
|
4176
|
-
return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
|
|
4177
|
-
}
|
|
4178
|
-
const xs = [0];
|
|
4179
|
-
for (let i = 1; i < children.length; i++) {
|
|
4180
|
-
xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
|
|
4181
|
-
}
|
|
4182
|
-
const first = children[0];
|
|
4183
|
-
const last = children[children.length - 1];
|
|
4184
|
-
const axis = (xs[0] + xs[xs.length - 1]) / 2;
|
|
4185
|
-
const offsets = xs.map((x) => x - axis);
|
|
4186
|
-
const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
|
|
4187
|
-
const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
|
|
4188
|
-
return { halfL, halfR, offsets };
|
|
4189
|
-
}
|
|
4371
|
+
var round12 = (n) => Math.round(n * 100) / 100;
|
|
4372
|
+
var drawnLeftEdge = (cx, boxW) => round12(round12(cx) - round12(boxW) / 2);
|
|
4373
|
+
var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) + round12(boxW);
|
|
4190
4374
|
function measurePosition(position, maxLabelChars, vacantWord) {
|
|
4191
4375
|
const nameLines = position.name === "" ? [] : wrapLabelBalanced(clampLabel(position.name, maxLabelChars), 2);
|
|
4192
4376
|
const titleLines = position.title === null ? [] : wrapLabelBalanced(clampLabel(position.title, maxLabelChars), 2);
|
|
@@ -4219,16 +4403,16 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4219
4403
|
const vacantWord = titleLabels.vacant;
|
|
4220
4404
|
const positionById = new Map(input.positions.map((p) => [p.id, p]));
|
|
4221
4405
|
const reports = [...input.reports].sort((a, b) => a.id - b.id);
|
|
4222
|
-
const solidReports = reports.filter((
|
|
4406
|
+
const solidReports = reports.filter((r2) => r2.kind !== "dotted");
|
|
4223
4407
|
const lineChildIds = /* @__PURE__ */ new Map();
|
|
4224
4408
|
const assistChildIds = /* @__PURE__ */ new Map();
|
|
4225
4409
|
const solidParentReportId = /* @__PURE__ */ new Set();
|
|
4226
|
-
for (const
|
|
4227
|
-
solidParentReportId.add(
|
|
4228
|
-
const bucket =
|
|
4229
|
-
const arr = bucket.get(
|
|
4230
|
-
arr.push(
|
|
4231
|
-
bucket.set(
|
|
4410
|
+
for (const r2 of solidReports) {
|
|
4411
|
+
solidParentReportId.add(r2.reportId);
|
|
4412
|
+
const bucket = r2.kind === "assistant" ? assistChildIds : lineChildIds;
|
|
4413
|
+
const arr = bucket.get(r2.managerId) ?? [];
|
|
4414
|
+
arr.push(r2);
|
|
4415
|
+
bucket.set(r2.managerId, arr);
|
|
4232
4416
|
}
|
|
4233
4417
|
for (const arr of lineChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
4234
4418
|
for (const arr of assistChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
@@ -4258,12 +4442,12 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4258
4442
|
const position = positionById.get(positionId);
|
|
4259
4443
|
const inst = newInst(position, depth, assistReport);
|
|
4260
4444
|
allInsts.push(inst);
|
|
4261
|
-
for (const
|
|
4262
|
-
const a = build(
|
|
4263
|
-
inst.assistants.push({ inst: a, report:
|
|
4445
|
+
for (const r2 of assistChildIds.get(positionId) ?? []) {
|
|
4446
|
+
const a = build(r2.reportId, depth + 1, r2);
|
|
4447
|
+
inst.assistants.push({ inst: a, report: r2 });
|
|
4264
4448
|
}
|
|
4265
|
-
for (const
|
|
4266
|
-
inst.lineChildren.push(build(
|
|
4449
|
+
for (const r2 of lineChildIds.get(positionId) ?? []) {
|
|
4450
|
+
inst.lineChildren.push(build(r2.reportId, depth + 1, null));
|
|
4267
4451
|
}
|
|
4268
4452
|
return inst;
|
|
4269
4453
|
};
|
|
@@ -4337,10 +4521,10 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4337
4521
|
const pushNode = (inst) => {
|
|
4338
4522
|
nodes.push({
|
|
4339
4523
|
positionId: inst.position.id,
|
|
4340
|
-
cx:
|
|
4341
|
-
top:
|
|
4342
|
-
boxW:
|
|
4343
|
-
boxH:
|
|
4524
|
+
cx: round12(inst.cx),
|
|
4525
|
+
top: round12(rowTop[inst.depth]),
|
|
4526
|
+
boxW: round12(inst.m.boxW),
|
|
4527
|
+
boxH: round12(inst.m.boxH),
|
|
4344
4528
|
style: inst.style,
|
|
4345
4529
|
nameLines: inst.m.nameLines,
|
|
4346
4530
|
titleLines: inst.m.titleLines,
|
|
@@ -4363,10 +4547,10 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4363
4547
|
const assistMidY = assistTop + a.m.boxH / 2;
|
|
4364
4548
|
nodes.push({
|
|
4365
4549
|
positionId: a.position.id,
|
|
4366
|
-
cx:
|
|
4367
|
-
top:
|
|
4368
|
-
boxW:
|
|
4369
|
-
boxH:
|
|
4550
|
+
cx: round12(a.cx),
|
|
4551
|
+
top: round12(assistTop),
|
|
4552
|
+
boxW: round12(a.m.boxW),
|
|
4553
|
+
boxH: round12(a.m.boxH),
|
|
4370
4554
|
style: a.style,
|
|
4371
4555
|
nameLines: a.m.nameLines,
|
|
4372
4556
|
titleLines: a.m.titleLines,
|
|
@@ -4381,8 +4565,8 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4381
4565
|
edgeId: ORG_ASSIST_ID_BASE + report.reportId,
|
|
4382
4566
|
kind: "assist",
|
|
4383
4567
|
points: [
|
|
4384
|
-
{ x:
|
|
4385
|
-
{ x: drawnRightEdge(a.cx, a.m.boxW), y:
|
|
4568
|
+
{ x: round12(inst.cx), y: round12(assistMidY) },
|
|
4569
|
+
{ x: drawnRightEdge(a.cx, a.m.boxW), y: round12(assistMidY) }
|
|
4386
4570
|
],
|
|
4387
4571
|
dashed: false,
|
|
4388
4572
|
title: reportTitle("assistant", report.label),
|
|
@@ -4398,8 +4582,8 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4398
4582
|
edgeId: ORG_STEM_ID_BASE + inst.position.id,
|
|
4399
4583
|
kind: "stem",
|
|
4400
4584
|
points: [
|
|
4401
|
-
{ x:
|
|
4402
|
-
{ x:
|
|
4585
|
+
{ x: round12(inst.cx), y: round12(boxBottom) },
|
|
4586
|
+
{ x: round12(inst.cx), y: round12(by) }
|
|
4403
4587
|
],
|
|
4404
4588
|
dashed: false,
|
|
4405
4589
|
title: reportTitle("line", null),
|
|
@@ -4411,8 +4595,8 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4411
4595
|
edgeId: ORG_BUS_ID_BASE + inst.position.id,
|
|
4412
4596
|
kind: "bus",
|
|
4413
4597
|
points: [
|
|
4414
|
-
{ x:
|
|
4415
|
-
{ x:
|
|
4598
|
+
{ x: round12(Math.min(...childCxs)), y: round12(by) },
|
|
4599
|
+
{ x: round12(Math.max(...childCxs)), y: round12(by) }
|
|
4416
4600
|
],
|
|
4417
4601
|
dashed: false,
|
|
4418
4602
|
title: reportTitle("line", null),
|
|
@@ -4420,15 +4604,15 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4420
4604
|
});
|
|
4421
4605
|
}
|
|
4422
4606
|
const lineReports = lineChildIds.get(inst.position.id) ?? [];
|
|
4423
|
-
const lineReportByChildId = new Map(lineReports.map((
|
|
4607
|
+
const lineReportByChildId = new Map(lineReports.map((r2) => [r2.reportId, r2]));
|
|
4424
4608
|
for (const c of inst.lineChildren) {
|
|
4425
4609
|
const lineReport = lineReportByChildId.get(c.position.id);
|
|
4426
4610
|
elements.push({
|
|
4427
4611
|
edgeId: ORG_DROP_ID_BASE + c.position.id,
|
|
4428
4612
|
kind: "drop",
|
|
4429
4613
|
points: [
|
|
4430
|
-
{ x:
|
|
4431
|
-
{ x:
|
|
4614
|
+
{ x: round12(c.cx), y: round12(by) },
|
|
4615
|
+
{ x: round12(c.cx), y: round12(rowTop[c.depth]) }
|
|
4432
4616
|
],
|
|
4433
4617
|
dashed: false,
|
|
4434
4618
|
title: reportTitle("line", null),
|
|
@@ -4466,7 +4650,7 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4466
4650
|
}
|
|
4467
4651
|
const nodeById = new Map(nodes.map((n) => [n.positionId, n]));
|
|
4468
4652
|
const dottedReports = reports.filter(
|
|
4469
|
-
(
|
|
4653
|
+
(r2) => r2.kind === "dotted" && nodeById.has(r2.managerId) && nodeById.has(r2.reportId) && r2.managerId !== r2.reportId
|
|
4470
4654
|
);
|
|
4471
4655
|
let maxBoxBottom = PADDING7;
|
|
4472
4656
|
for (const n of nodes) maxBoxBottom = Math.max(maxBoxBottom, n.top + n.boxH);
|
|
@@ -4474,14 +4658,14 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4474
4658
|
let channelLanes = 0;
|
|
4475
4659
|
if (dottedReports.length > 0) {
|
|
4476
4660
|
const laneOf = /* @__PURE__ */ new Map();
|
|
4477
|
-
laneCount =
|
|
4478
|
-
dottedReports.map((
|
|
4479
|
-
const rep = escapeByPos.get(
|
|
4480
|
-
const mgr = escapeByPos.get(
|
|
4661
|
+
laneCount = allocateLanes(
|
|
4662
|
+
dottedReports.map((r2) => {
|
|
4663
|
+
const rep = escapeByPos.get(r2.reportId);
|
|
4664
|
+
const mgr = escapeByPos.get(r2.managerId);
|
|
4481
4665
|
return {
|
|
4482
4666
|
lo: Math.min(rep.midY, mgr.midY),
|
|
4483
4667
|
hi: Math.max(rep.midY, mgr.midY),
|
|
4484
|
-
set: (lane) => laneOf.set(
|
|
4668
|
+
set: (lane) => laneOf.set(r2.id, lane)
|
|
4485
4669
|
};
|
|
4486
4670
|
})
|
|
4487
4671
|
);
|
|
@@ -4496,19 +4680,19 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4496
4680
|
const sign = slot % 2 === 1 ? -1 : 1;
|
|
4497
4681
|
return mid + sign * Math.ceil(slot / 2) * step;
|
|
4498
4682
|
};
|
|
4499
|
-
const geoms = dottedReports.map((
|
|
4500
|
-
const rep = escapeByPos.get(
|
|
4501
|
-
const mgr = escapeByPos.get(
|
|
4683
|
+
const geoms = dottedReports.map((r2, i) => {
|
|
4684
|
+
const rep = escapeByPos.get(r2.reportId);
|
|
4685
|
+
const mgr = escapeByPos.get(r2.managerId);
|
|
4502
4686
|
return {
|
|
4503
|
-
r,
|
|
4687
|
+
r: r2,
|
|
4504
4688
|
i,
|
|
4505
|
-
repMidY:
|
|
4506
|
-
mgrMidY:
|
|
4507
|
-
repBoxEdge:
|
|
4508
|
-
mgrBoxEdge:
|
|
4509
|
-
repChY:
|
|
4510
|
-
mgrChY:
|
|
4511
|
-
laneX:
|
|
4689
|
+
repMidY: round12(nextExitY(r2.reportId, rep.midY, rep.boxH)),
|
|
4690
|
+
mgrMidY: round12(nextExitY(r2.managerId, mgr.midY, mgr.boxH)),
|
|
4691
|
+
repBoxEdge: round12(rep.boxEdge),
|
|
4692
|
+
mgrBoxEdge: round12(mgr.boxEdge),
|
|
4693
|
+
repChY: round12(channelTop + 2 * i * ORG_MATRIX_LANE_PITCH),
|
|
4694
|
+
mgrChY: round12(channelTop + (2 * i + 1) * ORG_MATRIX_LANE_PITCH),
|
|
4695
|
+
laneX: round12(canvasRight + ORG_MATRIX_GUTTER_GAP + laneOf.get(r2.id) * ORG_MATRIX_LANE_PITCH),
|
|
4512
4696
|
repEscX: 0,
|
|
4513
4697
|
mgrEscX: 0
|
|
4514
4698
|
};
|
|
@@ -4546,14 +4730,14 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4546
4730
|
else if (v.side === -1 && bRight < v.escCol - VERT_EPS) band = Math.min(band, v.escCol - bRight);
|
|
4547
4731
|
}
|
|
4548
4732
|
let lane = 0;
|
|
4549
|
-
let candidate =
|
|
4733
|
+
let candidate = round12(v.escCol);
|
|
4550
4734
|
while (placed.some((p) => Math.abs(p.x - candidate) < VERT_EPS && yOverlap(p.lo, p.hi, v.lo, v.hi))) {
|
|
4551
4735
|
lane += 1;
|
|
4552
|
-
candidate =
|
|
4736
|
+
candidate = round12(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
|
|
4553
4737
|
}
|
|
4554
4738
|
if (lane > 0 && lane * ORG_MATRIX_ESCAPE_STEP >= band - VERT_EPS) {
|
|
4555
4739
|
throw new Error(
|
|
4556
|
-
`org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${
|
|
4740
|
+
`org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${round12(band)}px box-free band`
|
|
4557
4741
|
);
|
|
4558
4742
|
}
|
|
4559
4743
|
placed.push({ x: candidate, lo: v.lo, hi: v.hi });
|
|
@@ -4599,25 +4783,6 @@ function collapseDegenerate(points) {
|
|
|
4599
4783
|
}
|
|
4600
4784
|
return out;
|
|
4601
4785
|
}
|
|
4602
|
-
function allocateLanes2(items) {
|
|
4603
|
-
const lanes = [];
|
|
4604
|
-
for (const it of items) {
|
|
4605
|
-
let chosen = -1;
|
|
4606
|
-
for (let l = 0; l < lanes.length; l++) {
|
|
4607
|
-
if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
|
|
4608
|
-
chosen = l;
|
|
4609
|
-
break;
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
if (chosen === -1) {
|
|
4613
|
-
chosen = lanes.length;
|
|
4614
|
-
lanes.push([]);
|
|
4615
|
-
}
|
|
4616
|
-
lanes[chosen].push({ lo: it.lo, hi: it.hi });
|
|
4617
|
-
it.set(chosen);
|
|
4618
|
-
}
|
|
4619
|
-
return lanes.length;
|
|
4620
|
-
}
|
|
4621
4786
|
|
|
4622
4787
|
// src/org-chart/svg.ts
|
|
4623
4788
|
var GLYPH_STROKE5 = "#52525b";
|
|
@@ -4626,25 +4791,25 @@ var EDGE_INK7 = "#71717a";
|
|
|
4626
4791
|
var GLYPH_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="2"`;
|
|
4627
4792
|
var VACANT_DASH = `stroke-dasharray="6,4"`;
|
|
4628
4793
|
var DOTTED = EDGE_STROKE.distant;
|
|
4629
|
-
var
|
|
4794
|
+
var round13 = (n) => Math.round(n * 100) / 100;
|
|
4630
4795
|
var ORG_BOX_PAD_Y2 = 9;
|
|
4631
4796
|
function boxSvg(n) {
|
|
4632
|
-
const left =
|
|
4797
|
+
const left = round13(n.cx - n.boxW / 2);
|
|
4633
4798
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
4634
4799
|
if (n.style === "dashed") {
|
|
4635
4800
|
pieces.push(
|
|
4636
4801
|
`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`
|
|
4637
4802
|
);
|
|
4638
|
-
const ix =
|
|
4639
|
-
const iy =
|
|
4640
|
-
const iw =
|
|
4641
|
-
const ih =
|
|
4803
|
+
const ix = round13(left + 3);
|
|
4804
|
+
const iy = round13(n.top + 3);
|
|
4805
|
+
const iw = round13(n.boxW - 6);
|
|
4806
|
+
const ih = round13(n.boxH - 6);
|
|
4642
4807
|
pieces.push(`<rect x="${ix}" y="${iy}" width="${iw}" height="${ih}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`);
|
|
4643
4808
|
} else {
|
|
4644
4809
|
pieces.push(`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3}/>`);
|
|
4645
4810
|
}
|
|
4646
4811
|
let lineIndex = 0;
|
|
4647
|
-
const firstBaseline = (i) =>
|
|
4812
|
+
const firstBaseline = (i) => round13(n.top + ORG_BOX_PAD_Y2 + ORG_LABEL_LINE_H / 2 + i * ORG_LABEL_LINE_H + ORG_LABEL_FONT * 0.32);
|
|
4648
4813
|
const textBlock = (lines, font) => {
|
|
4649
4814
|
if (lines.length === 0) return "";
|
|
4650
4815
|
const tspans = lines.map((line, i) => `<tspan x="${n.cx}" y="${firstBaseline(lineIndex + i)}">${xmlEscape(line)}</tspan>`).join("");
|
|
@@ -4655,7 +4820,7 @@ function boxSvg(n) {
|
|
|
4655
4820
|
pieces.push(textBlock(n.titleLines, ORG_TITLE_FONT));
|
|
4656
4821
|
pieces.push(textBlock(n.subtitleLines, ORG_TITLE_FONT));
|
|
4657
4822
|
if (n.vacantMarker !== null) pieces.push(textBlock([n.vacantMarker], ORG_TITLE_FONT));
|
|
4658
|
-
if (n.annotated) pieces.push(annotationDot(
|
|
4823
|
+
if (n.annotated) pieces.push(annotationDot(round13(n.cx + n.boxW / 2) - 4, n.top + 4));
|
|
4659
4824
|
return `<g data-node-id="p${n.positionId}">${pieces.filter((p) => p !== "").join("")}</g>`;
|
|
4660
4825
|
}
|
|
4661
4826
|
function elementSvg5(el) {
|
|
@@ -4674,26 +4839,26 @@ function elementSvg5(el) {
|
|
|
4674
4839
|
}
|
|
4675
4840
|
var MINI_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="1.5"`;
|
|
4676
4841
|
function lineSwatch(x, y) {
|
|
4677
|
-
const x1 =
|
|
4678
|
-
const x2 =
|
|
4842
|
+
const x1 = round13(x + 2);
|
|
4843
|
+
const x2 = round13(x + LEGEND_SWATCH_W - 2);
|
|
4679
4844
|
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
4680
4845
|
}
|
|
4681
4846
|
function assistantSwatch(x, y) {
|
|
4682
|
-
const stemX =
|
|
4683
|
-
const boxX =
|
|
4684
|
-
return `<line x1="${stemX}" y1="${
|
|
4847
|
+
const stemX = round13(x + LEGEND_SWATCH_W - 4);
|
|
4848
|
+
const boxX = round13(x + 1);
|
|
4849
|
+
return `<line x1="${stemX}" y1="${round13(y - 4)}" x2="${stemX}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><line x1="${stemX}" y1="${y}" x2="${round13(boxX + 8)}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><rect x="${boxX}" y="${round13(y - 3.5)}" width="8" height="7" rx="1" ${MINI_ATTRS3}/>`;
|
|
4685
4850
|
}
|
|
4686
4851
|
function dottedSwatch(x, y) {
|
|
4687
|
-
const x1 =
|
|
4688
|
-
const x2 =
|
|
4852
|
+
const x1 = round13(x + 2);
|
|
4853
|
+
const x2 = round13(x + LEGEND_SWATCH_W - 2);
|
|
4689
4854
|
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
4690
4855
|
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>`;
|
|
4691
4856
|
}
|
|
4692
4857
|
function vacantSwatch(x, y) {
|
|
4693
|
-
const cx =
|
|
4694
|
-
const bx =
|
|
4695
|
-
const by =
|
|
4696
|
-
return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/><rect x="${
|
|
4858
|
+
const cx = round13(x + LEGEND_SWATCH_W / 2);
|
|
4859
|
+
const bx = round13(cx - 8);
|
|
4860
|
+
const by = round13(y - 5);
|
|
4861
|
+
return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/><rect x="${round13(bx + 2)}" y="${round13(by + 2)}" width="12" height="6" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/>`;
|
|
4697
4862
|
}
|
|
4698
4863
|
function orgChartLayoutSvg(layout, opts = {}) {
|
|
4699
4864
|
const labels = opts.labels ?? ORG_CHART_SVG_LABELS_EN;
|
|
@@ -4741,6 +4906,1654 @@ function orgChartSvg(input, opts = {}) {
|
|
|
4741
4906
|
return { svg, layout };
|
|
4742
4907
|
}
|
|
4743
4908
|
|
|
4909
|
+
// src/prisma/types.ts
|
|
4910
|
+
var PRISMA_PHASES = ["identification", "screening", "included"];
|
|
4911
|
+
var PRISMA_BOX_KINDS = ["flow", "exclusion"];
|
|
4912
|
+
var PRISMA_COLUMNS = ["main", "new", "previous"];
|
|
4913
|
+
|
|
4914
|
+
// src/prisma/validate.ts
|
|
4915
|
+
var PrismaValidationError = class extends Error {
|
|
4916
|
+
issues;
|
|
4917
|
+
constructor(issues) {
|
|
4918
|
+
super(`invalid PRISMA diagram: ${issues.map((i) => i.message).join("; ")}`);
|
|
4919
|
+
this.name = "PrismaValidationError";
|
|
4920
|
+
this.issues = issues;
|
|
4921
|
+
}
|
|
4922
|
+
};
|
|
4923
|
+
function sortIssues5(issues) {
|
|
4924
|
+
const unique = /* @__PURE__ */ new Map();
|
|
4925
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
4926
|
+
return [...unique.values()].sort(
|
|
4927
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
4928
|
+
);
|
|
4929
|
+
}
|
|
4930
|
+
function prismaIssues(input) {
|
|
4931
|
+
if (input.boxes.length === 0 && input.arrows.length === 0) return [];
|
|
4932
|
+
const issues = [];
|
|
4933
|
+
const push = (code, message) => {
|
|
4934
|
+
issues.push({ code, message });
|
|
4935
|
+
};
|
|
4936
|
+
const boxById = /* @__PURE__ */ new Map();
|
|
4937
|
+
const dupBoxIds = /* @__PURE__ */ new Set();
|
|
4938
|
+
for (const b of input.boxes) {
|
|
4939
|
+
if (boxById.has(b.id)) dupBoxIds.add(b.id);
|
|
4940
|
+
else boxById.set(b.id, b);
|
|
4941
|
+
}
|
|
4942
|
+
for (const id of [...dupBoxIds].sort((a, b) => a - b)) {
|
|
4943
|
+
push("duplicate-id", `duplicate box id ${id}`);
|
|
4944
|
+
}
|
|
4945
|
+
const arrowById = /* @__PURE__ */ new Map();
|
|
4946
|
+
const dupArrowIds = /* @__PURE__ */ new Set();
|
|
4947
|
+
for (const a of input.arrows) {
|
|
4948
|
+
if (arrowById.has(a.id)) dupArrowIds.add(a.id);
|
|
4949
|
+
else arrowById.set(a.id, a);
|
|
4950
|
+
}
|
|
4951
|
+
for (const id of [...dupArrowIds].sort((a, b) => a - b)) {
|
|
4952
|
+
push("duplicate-id", `duplicate arrow id ${id}`);
|
|
4953
|
+
}
|
|
4954
|
+
if (issues.length > 0) return sortIssues5(issues);
|
|
4955
|
+
for (const arrow of [...arrowById.values()].sort((a, b) => a.id - b.id)) {
|
|
4956
|
+
if (!boxById.has(arrow.fromId)) {
|
|
4957
|
+
push("unknown-arrow-endpoint", `arrow ${arrow.id} fromId ${arrow.fromId} is not a declared box`);
|
|
4958
|
+
}
|
|
4959
|
+
if (!boxById.has(arrow.toId)) {
|
|
4960
|
+
push("unknown-arrow-endpoint", `arrow ${arrow.id} toId ${arrow.toId} is not a declared box`);
|
|
4961
|
+
}
|
|
4962
|
+
if (arrow.fromId === arrow.toId) {
|
|
4963
|
+
push("self-arrow", `arrow ${arrow.id} has fromId === toId (${arrow.fromId})`);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
for (const box of [...boxById.values()].sort((a, b) => a.id - b.id)) {
|
|
4967
|
+
for (const count of box.counts) {
|
|
4968
|
+
if (count.n !== null && !Number.isFinite(count.n)) {
|
|
4969
|
+
push("non-finite-count", `box ${box.id} count "${count.label}" has non-finite n: ${String(count.n)}`);
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
if (input.variant !== "flow-with-prior") {
|
|
4974
|
+
for (const box of [...boxById.values()].sort((a, b) => a.id - b.id)) {
|
|
4975
|
+
if (box.column === "previous") {
|
|
4976
|
+
push(
|
|
4977
|
+
"invalid-variant-column",
|
|
4978
|
+
`box ${box.id} has column "previous" but variant is "${input.variant}"`
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4983
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
4984
|
+
"duplicate-id",
|
|
4985
|
+
"unknown-arrow-endpoint"
|
|
4986
|
+
]);
|
|
4987
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
4988
|
+
const phaseIndex = /* @__PURE__ */ new Map();
|
|
4989
|
+
for (let i = 0; i < PRISMA_PHASES.length; i++) phaseIndex.set(PRISMA_PHASES[i], i);
|
|
4990
|
+
for (const arrow of [...arrowById.values()].sort((a, b) => a.id - b.id)) {
|
|
4991
|
+
const fromBox = boxById.get(arrow.fromId);
|
|
4992
|
+
const toBox = boxById.get(arrow.toId);
|
|
4993
|
+
const fromPhaseIdx = phaseIndex.get(fromBox.phase);
|
|
4994
|
+
const toPhaseIdx = phaseIndex.get(toBox.phase);
|
|
4995
|
+
if (fromBox.kind === "flow" && toBox.kind === "exclusion" && fromBox.phase === toBox.phase) {
|
|
4996
|
+
continue;
|
|
4997
|
+
}
|
|
4998
|
+
if (toPhaseIdx < fromPhaseIdx) {
|
|
4999
|
+
push(
|
|
5000
|
+
"arrow-skips-backward",
|
|
5001
|
+
`arrow ${arrow.id} goes backward: from phase "${fromBox.phase}" to "${toBox.phase}"`
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
return sortIssues5(issues);
|
|
5007
|
+
}
|
|
5008
|
+
function validatePrisma(input) {
|
|
5009
|
+
const issues = prismaIssues(input);
|
|
5010
|
+
if (issues.length > 0) throw new PrismaValidationError(issues);
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
// src/prisma/labels.ts
|
|
5014
|
+
var PRISMA_TITLE_LABELS_EN = {
|
|
5015
|
+
phases: {
|
|
5016
|
+
identification: "Identification",
|
|
5017
|
+
screening: "Screening",
|
|
5018
|
+
included: "Included"
|
|
5019
|
+
},
|
|
5020
|
+
nullCount: "n = \u2014",
|
|
5021
|
+
arrowKinds: {
|
|
5022
|
+
flow: "Flow",
|
|
5023
|
+
exclusion: "Exclusion",
|
|
5024
|
+
merge: "Merge"
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
5027
|
+
var PRISMA_SVG_LABELS_EN = {
|
|
5028
|
+
legend: {
|
|
5029
|
+
flow: "Main flow",
|
|
5030
|
+
exclusion: "Excluded"
|
|
5031
|
+
},
|
|
5032
|
+
ariaLabel: "PRISMA 2020 flow diagram"
|
|
5033
|
+
};
|
|
5034
|
+
|
|
5035
|
+
// src/prisma/layout.ts
|
|
5036
|
+
var PRISMA_PADDING = 32;
|
|
5037
|
+
var PRISMA_BAND_LABEL_W = 40;
|
|
5038
|
+
var PRISMA_COL_GAP = 56;
|
|
5039
|
+
var PRISMA_CORRIDOR = 30;
|
|
5040
|
+
var PRISMA_BOX_PAD_X = 12;
|
|
5041
|
+
var PRISMA_BOX_PAD_Y = 10;
|
|
5042
|
+
var PRISMA_LINE_H = 14;
|
|
5043
|
+
var PRISMA_MIN_BOX_W = 200;
|
|
5044
|
+
var PRISMA_EXCL_GAP = 48;
|
|
5045
|
+
var PRISMA_HEADING_FONT = 12;
|
|
5046
|
+
var PRISMA_COUNT_FONT = 10;
|
|
5047
|
+
var PRISMA_ROUTE_MARGIN = 10;
|
|
5048
|
+
var PRISMA_ROUTE_PITCH = 10;
|
|
5049
|
+
var PRISMA_FLOW_ARROW_BASE = 1e6;
|
|
5050
|
+
var PRISMA_EXCL_ARROW_BASE = 2e6;
|
|
5051
|
+
var PRISMA_MERGE_ARROW_BASE = 3e6;
|
|
5052
|
+
function round14(n) {
|
|
5053
|
+
return Math.round(n * 100) / 100;
|
|
5054
|
+
}
|
|
5055
|
+
function wrapHeading(h) {
|
|
5056
|
+
if (h === null || h === "") return [];
|
|
5057
|
+
return wrapLabelBalanced(h);
|
|
5058
|
+
}
|
|
5059
|
+
function countLine(label, n, nullToken) {
|
|
5060
|
+
const nStr = n === null ? nullToken : String(n);
|
|
5061
|
+
return `${label}: n = ${nStr}`;
|
|
5062
|
+
}
|
|
5063
|
+
function buildCountLines(counts, nullToken) {
|
|
5064
|
+
return counts.map((c) => countLine(c.label, c.n, nullToken));
|
|
5065
|
+
}
|
|
5066
|
+
function boxTitle(box, nullToken) {
|
|
5067
|
+
if (box.title !== void 0) return box.title;
|
|
5068
|
+
const parts = [];
|
|
5069
|
+
if (box.heading !== null && box.heading !== "") parts.push(box.heading);
|
|
5070
|
+
for (const c of box.counts) parts.push(countLine(c.label, c.n, nullToken));
|
|
5071
|
+
return parts.join("; ") || "(box)";
|
|
5072
|
+
}
|
|
5073
|
+
function computePrismaLayout(input, opts = {}) {
|
|
5074
|
+
if (opts.validate !== false) validatePrisma(input);
|
|
5075
|
+
const labels = opts.labels ?? PRISMA_TITLE_LABELS_EN;
|
|
5076
|
+
const nullToken = labels.nullCount;
|
|
5077
|
+
const COMP_OPTS = {
|
|
5078
|
+
padX: PRISMA_BOX_PAD_X,
|
|
5079
|
+
padY: PRISMA_BOX_PAD_Y,
|
|
5080
|
+
lineH: PRISMA_LINE_H,
|
|
5081
|
+
minW: PRISMA_MIN_BOX_W};
|
|
5082
|
+
const boxMetrics = /* @__PURE__ */ new Map();
|
|
5083
|
+
for (const b of input.boxes) {
|
|
5084
|
+
const headingLines = wrapHeading(b.heading);
|
|
5085
|
+
const cLines = buildCountLines(b.counts, nullToken);
|
|
5086
|
+
const metrics = measureCompartmentBox(
|
|
5087
|
+
[
|
|
5088
|
+
{ lines: headingLines, font: PRISMA_HEADING_FONT },
|
|
5089
|
+
{ lines: cLines, font: PRISMA_COUNT_FONT }
|
|
5090
|
+
],
|
|
5091
|
+
COMP_OPTS
|
|
5092
|
+
);
|
|
5093
|
+
boxMetrics.set(b.id, {
|
|
5094
|
+
boxW: metrics.boxW,
|
|
5095
|
+
boxH: metrics.boxH,
|
|
5096
|
+
rows: metrics.rows,
|
|
5097
|
+
dividerYs: metrics.dividerYs,
|
|
5098
|
+
headingLines,
|
|
5099
|
+
countLines: cLines
|
|
5100
|
+
});
|
|
5101
|
+
}
|
|
5102
|
+
const boxesByKey = /* @__PURE__ */ new Map();
|
|
5103
|
+
for (const b of input.boxes) {
|
|
5104
|
+
const key = `${b.phase}:${b.column}:${b.kind}`;
|
|
5105
|
+
const arr = boxesByKey.get(key) ?? [];
|
|
5106
|
+
arr.push(b);
|
|
5107
|
+
boxesByKey.set(key, arr);
|
|
5108
|
+
}
|
|
5109
|
+
for (const arr of boxesByKey.values()) {
|
|
5110
|
+
arr.sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.id - b.id);
|
|
5111
|
+
}
|
|
5112
|
+
const groupOf = (phase, col, kind) => boxesByKey.get(`${phase}:${col}:${kind}`) ?? [];
|
|
5113
|
+
const flowCols = input.variant === "flow-with-prior" ? ["new", "previous"] : input.variant === "flow-other-methods" ? ["main", "new"] : ["main"];
|
|
5114
|
+
const contentLeft = PRISMA_PADDING + PRISMA_BAND_LABEL_W;
|
|
5115
|
+
function maxColW(col, kind) {
|
|
5116
|
+
let w = 0;
|
|
5117
|
+
for (const phase of PRISMA_PHASES) {
|
|
5118
|
+
for (const b of groupOf(phase, col, kind)) {
|
|
5119
|
+
const bw = boxMetrics.get(b.id).boxW;
|
|
5120
|
+
if (bw > w) w = bw;
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
return w;
|
|
5124
|
+
}
|
|
5125
|
+
const colHasFlow = (col) => PRISMA_PHASES.some((phase) => groupOf(phase, col, "flow").length > 0);
|
|
5126
|
+
const colHasExcl = (col) => PRISMA_PHASES.some((phase) => groupOf(phase, col, "exclusion").length > 0);
|
|
5127
|
+
const exclOnlyCols = ["main", "new", "previous"].filter(
|
|
5128
|
+
(col) => !flowCols.includes(col) && colHasExcl(col)
|
|
5129
|
+
);
|
|
5130
|
+
const placedCols = [...flowCols, ...exclOnlyCols];
|
|
5131
|
+
const colGeom = /* @__PURE__ */ new Map();
|
|
5132
|
+
for (const col of placedCols) {
|
|
5133
|
+
const flowW = colHasFlow(col) ? Math.max(PRISMA_MIN_BOX_W, maxColW(col, "flow")) : 0;
|
|
5134
|
+
const exclW = maxColW(col, "exclusion");
|
|
5135
|
+
const blockW = flowW > 0 && exclW > 0 ? flowW + PRISMA_EXCL_GAP + exclW : flowW > 0 ? flowW : exclW;
|
|
5136
|
+
colGeom.set(col, { flowW, exclW, blockW, flowCx: 0, exclCx: 0 });
|
|
5137
|
+
}
|
|
5138
|
+
function placeBlock(col, blockLeft) {
|
|
5139
|
+
const g = colGeom.get(col);
|
|
5140
|
+
if (g.flowW > 0) g.flowCx = round14(blockLeft + g.flowW / 2);
|
|
5141
|
+
if (g.exclW > 0) {
|
|
5142
|
+
const exclLeft = g.flowW > 0 ? blockLeft + g.flowW + PRISMA_EXCL_GAP : blockLeft;
|
|
5143
|
+
g.exclCx = round14(exclLeft + g.exclW / 2);
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
if (placedCols.length === 1) {
|
|
5147
|
+
placeBlock(placedCols[0], contentLeft);
|
|
5148
|
+
} else {
|
|
5149
|
+
const packed = packSubtree(
|
|
5150
|
+
{
|
|
5151
|
+
ownHalfL: 0,
|
|
5152
|
+
ownHalfR: 0,
|
|
5153
|
+
children: placedCols.map((col) => {
|
|
5154
|
+
const half = colGeom.get(col).blockW / 2;
|
|
5155
|
+
return { halfL: half, halfR: half };
|
|
5156
|
+
})
|
|
5157
|
+
},
|
|
5158
|
+
{ siblingGap: PRISMA_COL_GAP }
|
|
5159
|
+
);
|
|
5160
|
+
const totalW = packed.halfL + packed.halfR;
|
|
5161
|
+
const blockCx = round14(contentLeft + totalW / 2);
|
|
5162
|
+
placedCols.forEach((col, i) => {
|
|
5163
|
+
const w = colGeom.get(col).blockW;
|
|
5164
|
+
placeBlock(col, round14(blockCx + packed.offsets[i] - w / 2));
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
let totalRight = contentLeft;
|
|
5168
|
+
for (const col of placedCols) {
|
|
5169
|
+
const g = colGeom.get(col);
|
|
5170
|
+
const r2 = g.exclW > 0 ? round14(g.exclCx + g.exclW / 2) : round14(g.flowCx + g.flowW / 2);
|
|
5171
|
+
if (r2 > totalRight) totalRight = r2;
|
|
5172
|
+
}
|
|
5173
|
+
const rowsByCol = /* @__PURE__ */ new Map();
|
|
5174
|
+
const rowsKey = (phase, col) => `${phase}:${col}`;
|
|
5175
|
+
function buildRankRows(phase, col) {
|
|
5176
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
5177
|
+
for (const kind of ["flow", "exclusion"]) {
|
|
5178
|
+
for (const b of groupOf(phase, col, kind)) {
|
|
5179
|
+
const h = boxMetrics.get(b.id).boxH;
|
|
5180
|
+
const prev = byRank.get(b.rank) ?? 0;
|
|
5181
|
+
if (h > prev) byRank.set(b.rank, h);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
const ranks = [...byRank.keys()].sort((a, b) => a - b);
|
|
5185
|
+
return ranks.map((rank) => ({ rank, rowH: byRank.get(rank), top: 0 }));
|
|
5186
|
+
}
|
|
5187
|
+
for (const phase of PRISMA_PHASES) {
|
|
5188
|
+
for (const col of placedCols) {
|
|
5189
|
+
rowsByCol.set(rowsKey(phase, col), buildRankRows(phase, col));
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
function colStackH(rows) {
|
|
5193
|
+
if (rows.length === 0) return 0;
|
|
5194
|
+
let h = 0;
|
|
5195
|
+
for (const r2 of rows) h += r2.rowH + PRISMA_BOX_PAD_Y;
|
|
5196
|
+
return h - PRISMA_BOX_PAD_Y;
|
|
5197
|
+
}
|
|
5198
|
+
function bandRowH(phase) {
|
|
5199
|
+
let maxH = 0;
|
|
5200
|
+
let any = false;
|
|
5201
|
+
for (const col of placedCols) {
|
|
5202
|
+
const rows = rowsByCol.get(rowsKey(phase, col));
|
|
5203
|
+
if (rows.length > 0) any = true;
|
|
5204
|
+
const h = colStackH(rows);
|
|
5205
|
+
if (h > maxH) maxH = h;
|
|
5206
|
+
}
|
|
5207
|
+
if (!any) return PRISMA_LINE_H * 2 + PRISMA_BOX_PAD_Y * 4;
|
|
5208
|
+
return maxH;
|
|
5209
|
+
}
|
|
5210
|
+
const bandRowHs = PRISMA_PHASES.map((phase) => ({
|
|
5211
|
+
rowH: bandRowH(phase),
|
|
5212
|
+
zoneH: 0
|
|
5213
|
+
}));
|
|
5214
|
+
const stacked = bandStack(bandRowHs, { padding: PRISMA_PADDING, corridor: PRISMA_CORRIDOR });
|
|
5215
|
+
const layoutBoxes = [];
|
|
5216
|
+
const lboxById = /* @__PURE__ */ new Map();
|
|
5217
|
+
function placeBox(b, phase, kind, cx, top) {
|
|
5218
|
+
const m = boxMetrics.get(b.id);
|
|
5219
|
+
const t = round14(top);
|
|
5220
|
+
const title = boxTitle(b, nullToken);
|
|
5221
|
+
const lbox = {
|
|
5222
|
+
id: b.id,
|
|
5223
|
+
phase,
|
|
5224
|
+
kind,
|
|
5225
|
+
cx: round14(cx),
|
|
5226
|
+
top: t,
|
|
5227
|
+
boxW: m.boxW,
|
|
5228
|
+
boxH: m.boxH,
|
|
5229
|
+
rows: m.rows.map((r2) => ({ ...r2, top: round14(t + r2.top) })),
|
|
5230
|
+
dividerYs: m.dividerYs.map((d) => round14(t + d)),
|
|
5231
|
+
heading: b.heading,
|
|
5232
|
+
countLines: m.countLines,
|
|
5233
|
+
title
|
|
5234
|
+
};
|
|
5235
|
+
layoutBoxes.push(lbox);
|
|
5236
|
+
lboxById.set(b.id, lbox);
|
|
5237
|
+
}
|
|
5238
|
+
for (let phaseIdx = 0; phaseIdx < PRISMA_PHASES.length; phaseIdx++) {
|
|
5239
|
+
const phase = PRISMA_PHASES[phaseIdx];
|
|
5240
|
+
const bandTop = stacked.rowTop[phaseIdx];
|
|
5241
|
+
for (const col of placedCols) {
|
|
5242
|
+
const g = colGeom.get(col);
|
|
5243
|
+
const rows = rowsByCol.get(rowsKey(phase, col));
|
|
5244
|
+
const topByRank = /* @__PURE__ */ new Map();
|
|
5245
|
+
let y = bandTop;
|
|
5246
|
+
for (const r2 of rows) {
|
|
5247
|
+
r2.top = round14(y);
|
|
5248
|
+
topByRank.set(r2.rank, r2.top);
|
|
5249
|
+
y = round14(y + r2.rowH + PRISMA_BOX_PAD_Y);
|
|
5250
|
+
}
|
|
5251
|
+
if (g.flowW > 0) {
|
|
5252
|
+
for (const b of groupOf(phase, col, "flow")) {
|
|
5253
|
+
placeBox(b, phase, "flow", g.flowCx, topByRank.get(b.rank));
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
if (g.exclW > 0) {
|
|
5257
|
+
for (const b of groupOf(phase, col, "exclusion")) {
|
|
5258
|
+
placeBox(b, phase, "exclusion", g.exclCx, topByRank.get(b.rank));
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
const elements = [];
|
|
5264
|
+
const boxById = new Map(input.boxes.map((b) => [b.id, b]));
|
|
5265
|
+
const phaseIndexMap = new Map(
|
|
5266
|
+
PRISMA_PHASES.map((p, i) => [p, i])
|
|
5267
|
+
);
|
|
5268
|
+
function boxBottom(lb) {
|
|
5269
|
+
return round14(lb.top + lb.boxH);
|
|
5270
|
+
}
|
|
5271
|
+
function bandBottom(phase) {
|
|
5272
|
+
const idx = PRISMA_PHASES.indexOf(phase);
|
|
5273
|
+
return round14(stacked.rowTop[idx] + bandRowHs[idx].rowH);
|
|
5274
|
+
}
|
|
5275
|
+
const arrowsToBox = /* @__PURE__ */ new Map();
|
|
5276
|
+
for (const arrow of input.arrows) {
|
|
5277
|
+
const toBox = boxById.get(arrow.toId);
|
|
5278
|
+
const fromBox = boxById.get(arrow.fromId);
|
|
5279
|
+
if (toBox === void 0 || fromBox === void 0) continue;
|
|
5280
|
+
if (fromBox.kind === "flow" && toBox.kind === "flow") {
|
|
5281
|
+
const arr = arrowsToBox.get(arrow.toId) ?? [];
|
|
5282
|
+
arr.push(arrow.id);
|
|
5283
|
+
arrowsToBox.set(arrow.toId, arr);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
const arrowToBoxArrivalIdx = /* @__PURE__ */ new Map();
|
|
5287
|
+
const exclArrowIndexBySource = /* @__PURE__ */ new Map();
|
|
5288
|
+
for (const arrow of input.arrows) {
|
|
5289
|
+
const fromLBox = lboxById.get(arrow.fromId);
|
|
5290
|
+
const toLBox = lboxById.get(arrow.toId);
|
|
5291
|
+
if (fromLBox === void 0 || toLBox === void 0) continue;
|
|
5292
|
+
const fromBox = boxById.get(arrow.fromId);
|
|
5293
|
+
const toBox = boxById.get(arrow.toId);
|
|
5294
|
+
const isExclusion = fromBox.kind === "flow" && toBox.kind === "exclusion";
|
|
5295
|
+
const isFlowToFlow = fromBox.kind === "flow" && toBox.kind === "flow";
|
|
5296
|
+
if (isExclusion) {
|
|
5297
|
+
const idx = exclArrowIndexBySource.get(arrow.fromId) ?? 0;
|
|
5298
|
+
exclArrowIndexBySource.set(arrow.fromId, idx + 1);
|
|
5299
|
+
const srcRight = round14(fromLBox.cx + fromLBox.boxW / 2);
|
|
5300
|
+
const exclMidY = round14(toLBox.top + toLBox.boxH / 2);
|
|
5301
|
+
const eLeft = round14(toLBox.cx - toLBox.boxW / 2);
|
|
5302
|
+
const sameColumn = fromBox.column === toBox.column;
|
|
5303
|
+
let points;
|
|
5304
|
+
if (sameColumn) {
|
|
5305
|
+
points = [
|
|
5306
|
+
{ x: round14(srcRight), y: round14(exclMidY) },
|
|
5307
|
+
{ x: round14(eLeft), y: round14(exclMidY) }
|
|
5308
|
+
];
|
|
5309
|
+
} else {
|
|
5310
|
+
const colGapStep = round14(PRISMA_COL_GAP / 8);
|
|
5311
|
+
const exclGapStep = round14(PRISMA_EXCL_GAP / 8);
|
|
5312
|
+
const fenceX = round14(srcRight + (idx + 1) * colGapStep);
|
|
5313
|
+
const exclFenceX = round14(eLeft - (idx + 1) * exclGapStep);
|
|
5314
|
+
const bBotY = bandBottom(fromBox.phase);
|
|
5315
|
+
const corridorY = round14(bBotY + PRISMA_ROUTE_MARGIN + idx * PRISMA_ROUTE_PITCH);
|
|
5316
|
+
points = [
|
|
5317
|
+
{ x: round14(srcRight), y: round14(exclMidY) },
|
|
5318
|
+
// exit source right at excl center y
|
|
5319
|
+
{ x: round14(fenceX), y: round14(exclMidY) },
|
|
5320
|
+
// horizontal into inter-col gap (unique y)
|
|
5321
|
+
{ x: round14(fenceX), y: round14(corridorY) },
|
|
5322
|
+
// vertical at unique x in inter-col gap
|
|
5323
|
+
{ x: round14(exclFenceX), y: round14(corridorY) },
|
|
5324
|
+
// horizontal in corridor (unique y)
|
|
5325
|
+
{ x: round14(exclFenceX), y: round14(exclMidY) },
|
|
5326
|
+
// vertical at unique x left of excl box
|
|
5327
|
+
{ x: round14(eLeft), y: round14(exclMidY) }
|
|
5328
|
+
// horizontal into excl box (unique y)
|
|
5329
|
+
];
|
|
5330
|
+
}
|
|
5331
|
+
elements.push({
|
|
5332
|
+
edgeId: PRISMA_EXCL_ARROW_BASE + arrow.id,
|
|
5333
|
+
kind: "exclusion",
|
|
5334
|
+
points,
|
|
5335
|
+
title: `${labels.arrowKinds.exclusion}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5336
|
+
headDir: "right"
|
|
5337
|
+
});
|
|
5338
|
+
} else if (isFlowToFlow) {
|
|
5339
|
+
const fromPhaseIdx = phaseIndexMap.get(fromBox.phase);
|
|
5340
|
+
const toPhaseIdx = phaseIndexMap.get(toBox.phase);
|
|
5341
|
+
const isSamePhase = fromBox.phase === toBox.phase;
|
|
5342
|
+
const isSameCol = fromBox.column === toBox.column;
|
|
5343
|
+
const isForward = toPhaseIdx >= fromPhaseIdx;
|
|
5344
|
+
const arrivals = arrowsToBox.get(arrow.toId) ?? [];
|
|
5345
|
+
let arrivalIdx = arrowToBoxArrivalIdx.get(arrow.id) ?? 0;
|
|
5346
|
+
if (!arrowToBoxArrivalIdx.has(arrow.id)) {
|
|
5347
|
+
arrivalIdx = arrivals.indexOf(arrow.id);
|
|
5348
|
+
arrowToBoxArrivalIdx.set(arrow.id, arrivalIdx);
|
|
5349
|
+
}
|
|
5350
|
+
const nArrivals = arrivals.length;
|
|
5351
|
+
const APPROACH_SPREAD = round14(toLBox.boxW / 4);
|
|
5352
|
+
const spreadStep = nArrivals > 1 ? round14(APPROACH_SPREAD / (nArrivals - 1)) : 0;
|
|
5353
|
+
const approachX = round14(toLBox.cx - APPROACH_SPREAD / 2 + arrivalIdx * spreadStep);
|
|
5354
|
+
const toTop = toLBox.top;
|
|
5355
|
+
if (isSamePhase && isSameCol) {
|
|
5356
|
+
const fromBottom = boxBottom(fromLBox);
|
|
5357
|
+
const fromCx = round14(fromLBox.cx);
|
|
5358
|
+
const toCx = round14(toLBox.cx);
|
|
5359
|
+
const dropX = fromCx === toCx ? fromCx : toCx;
|
|
5360
|
+
elements.push({
|
|
5361
|
+
edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
|
|
5362
|
+
kind: "flow",
|
|
5363
|
+
points: [
|
|
5364
|
+
{ x: fromCx, y: round14(fromBottom) },
|
|
5365
|
+
{ x: dropX, y: round14(toTop) }
|
|
5366
|
+
],
|
|
5367
|
+
title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5368
|
+
headDir: "down"
|
|
5369
|
+
});
|
|
5370
|
+
} else if (isSameCol && isForward) {
|
|
5371
|
+
const fromBottom = boxBottom(fromLBox);
|
|
5372
|
+
if (nArrivals <= 1 && round14(fromLBox.cx) === round14(toLBox.cx)) {
|
|
5373
|
+
elements.push({
|
|
5374
|
+
edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
|
|
5375
|
+
kind: "flow",
|
|
5376
|
+
points: [
|
|
5377
|
+
{ x: round14(fromLBox.cx), y: round14(fromBottom) },
|
|
5378
|
+
{ x: round14(toLBox.cx), y: round14(toTop) }
|
|
5379
|
+
],
|
|
5380
|
+
title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5381
|
+
headDir: "down"
|
|
5382
|
+
});
|
|
5383
|
+
} else {
|
|
5384
|
+
const phIdx = phaseIndexMap.get(fromBox.phase);
|
|
5385
|
+
const elbowY = round14(stacked.rowTop[phIdx] + bandRowHs[phIdx].rowH + PRISMA_CORRIDOR / 2);
|
|
5386
|
+
elements.push({
|
|
5387
|
+
edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
|
|
5388
|
+
kind: "flow",
|
|
5389
|
+
points: [
|
|
5390
|
+
{ x: round14(fromLBox.cx), y: round14(fromBottom) },
|
|
5391
|
+
{ x: round14(fromLBox.cx), y: round14(elbowY) },
|
|
5392
|
+
{ x: round14(approachX), y: round14(elbowY) },
|
|
5393
|
+
{ x: round14(approachX), y: round14(toTop) }
|
|
5394
|
+
],
|
|
5395
|
+
title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5396
|
+
headDir: "down"
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5399
|
+
} else {
|
|
5400
|
+
const fromBottom = boxBottom(fromLBox);
|
|
5401
|
+
const phIdx = phaseIndexMap.get(fromBox.phase);
|
|
5402
|
+
const elbowY = round14(stacked.rowTop[phIdx] + bandRowHs[phIdx].rowH + PRISMA_CORRIDOR / 2);
|
|
5403
|
+
const fromCx = round14(fromLBox.cx);
|
|
5404
|
+
if (round14(fromCx) === round14(approachX)) {
|
|
5405
|
+
elements.push({
|
|
5406
|
+
edgeId: PRISMA_MERGE_ARROW_BASE + arrow.id,
|
|
5407
|
+
kind: "merge",
|
|
5408
|
+
points: [
|
|
5409
|
+
{ x: fromCx, y: round14(fromBottom) },
|
|
5410
|
+
{ x: round14(approachX), y: round14(toTop) }
|
|
5411
|
+
],
|
|
5412
|
+
title: `${labels.arrowKinds.merge}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5413
|
+
headDir: "down"
|
|
5414
|
+
});
|
|
5415
|
+
} else {
|
|
5416
|
+
elements.push({
|
|
5417
|
+
edgeId: PRISMA_MERGE_ARROW_BASE + arrow.id,
|
|
5418
|
+
kind: "merge",
|
|
5419
|
+
points: [
|
|
5420
|
+
{ x: fromCx, y: round14(fromBottom) },
|
|
5421
|
+
{ x: fromCx, y: round14(elbowY) },
|
|
5422
|
+
{ x: round14(approachX), y: round14(elbowY) },
|
|
5423
|
+
{ x: round14(approachX), y: round14(toTop) }
|
|
5424
|
+
],
|
|
5425
|
+
title: `${labels.arrowKinds.merge}: ${fromLBox.title} \u2192 ${toLBox.title}`,
|
|
5426
|
+
headDir: "down"
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
const canvasH = stacked.height;
|
|
5433
|
+
const canvasW = round14(totalRight + PRISMA_PADDING);
|
|
5434
|
+
const bands = PRISMA_PHASES.map((phase, i) => {
|
|
5435
|
+
const rowTop = stacked.rowTop[i];
|
|
5436
|
+
const rowH = bandRowHs[i].rowH;
|
|
5437
|
+
return {
|
|
5438
|
+
phase,
|
|
5439
|
+
rowTop,
|
|
5440
|
+
rowH,
|
|
5441
|
+
labelX: PRISMA_PADDING,
|
|
5442
|
+
centerY: round14(rowTop + rowH / 2)
|
|
5443
|
+
};
|
|
5444
|
+
});
|
|
5445
|
+
return {
|
|
5446
|
+
width: canvasW,
|
|
5447
|
+
height: canvasH,
|
|
5448
|
+
boxes: layoutBoxes,
|
|
5449
|
+
elements,
|
|
5450
|
+
bands,
|
|
5451
|
+
variant: input.variant
|
|
5452
|
+
};
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
// src/prisma/svg.ts
|
|
5456
|
+
var BOX_STROKE2 = "#52525b";
|
|
5457
|
+
var BOX_STROKE_W = 2;
|
|
5458
|
+
var BOX_FILL = "#fff";
|
|
5459
|
+
var BOX_RX = 2;
|
|
5460
|
+
var HEADING_FILL = "#3f3f46";
|
|
5461
|
+
var COUNT_FILL = "#52525b";
|
|
5462
|
+
var ARROW_STROKE = "#71717a";
|
|
5463
|
+
var ARROW_STROKE_W = 1.5;
|
|
5464
|
+
var ARROW_FILL = "#71717a";
|
|
5465
|
+
var BAND_LABEL_FILL = "#52525b";
|
|
5466
|
+
var BAND_LABEL_FONT = 11;
|
|
5467
|
+
function round15(n) {
|
|
5468
|
+
return Math.round(n * 100) / 100;
|
|
5469
|
+
}
|
|
5470
|
+
function emitBox(b) {
|
|
5471
|
+
const x = round15(b.cx - b.boxW / 2);
|
|
5472
|
+
const y = b.top;
|
|
5473
|
+
const parts = [];
|
|
5474
|
+
parts.push(
|
|
5475
|
+
`<rect x="${x}" y="${y}" width="${b.boxW}" height="${b.boxH}" rx="${BOX_RX}" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="${BOX_STROKE_W}"/>`
|
|
5476
|
+
);
|
|
5477
|
+
for (const dy of b.dividerYs) {
|
|
5478
|
+
parts.push(
|
|
5479
|
+
`<line x1="${x}" y1="${dy}" x2="${round15(x + b.boxW)}" y2="${dy}" stroke="${BOX_STROKE2}" stroke-width="1"/>`
|
|
5480
|
+
);
|
|
5481
|
+
}
|
|
5482
|
+
for (let ri = 0; ri < b.rows.length; ri++) {
|
|
5483
|
+
const row = b.rows[ri];
|
|
5484
|
+
const isHeading = ri === 0;
|
|
5485
|
+
const font = row.font;
|
|
5486
|
+
const fill = isHeading ? HEADING_FILL : COUNT_FILL;
|
|
5487
|
+
const textX = isHeading ? round15(b.cx) : round15(x + 8);
|
|
5488
|
+
for (let li = 0; li < row.lines.length; li++) {
|
|
5489
|
+
const line = row.lines[li];
|
|
5490
|
+
const textY = round15(row.top + li * 14 + font);
|
|
5491
|
+
const anchorAttr = isHeading ? ` text-anchor="middle"` : "";
|
|
5492
|
+
const weightAttr = isHeading ? ` font-weight="bold"` : "";
|
|
5493
|
+
parts.push(
|
|
5494
|
+
`<text x="${textX}" y="${textY}" font-family="${FONT_FAMILY}" font-size="${font}" fill="${fill}"${anchorAttr}${weightAttr}>${xmlEscape(line)}</text>`
|
|
5495
|
+
);
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
return parts.join("");
|
|
5499
|
+
}
|
|
5500
|
+
function emitArrow(el) {
|
|
5501
|
+
const points = el.points;
|
|
5502
|
+
if (points.length < 2) return "";
|
|
5503
|
+
const inset = glyphInset(el.headDir, "arrow");
|
|
5504
|
+
const last = points[points.length - 1];
|
|
5505
|
+
let lineEndX = last.x;
|
|
5506
|
+
let lineEndY = last.y;
|
|
5507
|
+
if (el.headDir === "down") lineEndY = round15(last.y - inset);
|
|
5508
|
+
else if (el.headDir === "right") lineEndX = round15(last.x - inset);
|
|
5509
|
+
else if (el.headDir === "left") lineEndX = round15(last.x + inset);
|
|
5510
|
+
else lineEndY = round15(last.y + inset);
|
|
5511
|
+
const linePoints = [
|
|
5512
|
+
...points.slice(0, points.length - 1).map((p) => ({ x: p.x, y: p.y })),
|
|
5513
|
+
{ x: lineEndX, y: lineEndY }
|
|
5514
|
+
];
|
|
5515
|
+
const pathStr = pathData(linePoints);
|
|
5516
|
+
const arrowPts = arrowFilledPoints({ x: last.x, y: last.y }, el.headDir);
|
|
5517
|
+
return `<title>${xmlEscape(el.title)}</title><path d="${pathStr}" stroke="${ARROW_STROKE}" stroke-width="${ARROW_STROKE_W}" fill="none"/><polygon points="${arrowPts}" fill="${ARROW_FILL}" stroke="none"/>`;
|
|
5518
|
+
}
|
|
5519
|
+
function emitBandLabel(phase, labelX, centerY, bandTitleLabels) {
|
|
5520
|
+
const text = bandTitleLabels.phases[phase];
|
|
5521
|
+
const gutterCx = round15(labelX + PRISMA_BAND_LABEL_W / 2);
|
|
5522
|
+
return `<text x="${gutterCx}" y="${round15(centerY)}" font-family="${FONT_FAMILY}" font-size="${BAND_LABEL_FONT}" fill="${BAND_LABEL_FILL}" text-anchor="middle" transform="rotate(-90,${gutterCx},${round15(centerY)})">${xmlEscape(text)}</text>`;
|
|
5523
|
+
}
|
|
5524
|
+
function buildLegendEntries(layout, svgLabels) {
|
|
5525
|
+
const hasFlow = layout.boxes.some((b) => b.kind === "flow");
|
|
5526
|
+
const hasExcl = layout.boxes.some((b) => b.kind === "exclusion");
|
|
5527
|
+
const entries = [];
|
|
5528
|
+
if (hasFlow) {
|
|
5529
|
+
entries.push({
|
|
5530
|
+
label: svgLabels.legend.flow,
|
|
5531
|
+
swatch: (x, cy) => `<rect x="${x}" y="${round15(cy - 6)}" width="22" height="12" rx="2" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="2"/>`
|
|
5532
|
+
});
|
|
5533
|
+
}
|
|
5534
|
+
if (hasExcl) {
|
|
5535
|
+
entries.push({
|
|
5536
|
+
label: svgLabels.legend.exclusion,
|
|
5537
|
+
swatch: (x, cy) => `<rect x="${x}" y="${round15(cy - 6)}" width="22" height="12" rx="2" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="2" stroke-dasharray="4,2"/>`
|
|
5538
|
+
});
|
|
5539
|
+
}
|
|
5540
|
+
return entries;
|
|
5541
|
+
}
|
|
5542
|
+
function prismaLayoutSvg(layout, opts = {}) {
|
|
5543
|
+
const svgLabels = opts.svgLabels ?? PRISMA_SVG_LABELS_EN;
|
|
5544
|
+
const bandTitleLabels = opts.titleLabels ?? PRISMA_TITLE_LABELS_EN;
|
|
5545
|
+
const doLegend = opts.legend !== false;
|
|
5546
|
+
const parts = [];
|
|
5547
|
+
for (const band of layout.bands) {
|
|
5548
|
+
parts.push(emitBandLabel(band.phase, band.labelX, band.centerY, bandTitleLabels));
|
|
5549
|
+
}
|
|
5550
|
+
for (const b of layout.boxes) {
|
|
5551
|
+
const boxSvg3 = emitBox(b);
|
|
5552
|
+
parts.push(`<g data-node-id="b${b.id}"><title>${xmlEscape(b.title)}</title>${boxSvg3}</g>`);
|
|
5553
|
+
}
|
|
5554
|
+
for (const el of layout.elements) {
|
|
5555
|
+
const arrowSvg = emitArrow(el);
|
|
5556
|
+
parts.push(`<g data-edge-id="${el.edgeId}">${arrowSvg}</g>`);
|
|
5557
|
+
}
|
|
5558
|
+
let w = layout.width;
|
|
5559
|
+
let h = layout.height;
|
|
5560
|
+
let legendSvg = "";
|
|
5561
|
+
if (doLegend) {
|
|
5562
|
+
const entries = buildLegendEntries(layout, svgLabels);
|
|
5563
|
+
const lb = legendBlock(entries, h);
|
|
5564
|
+
legendSvg = lb.svg;
|
|
5565
|
+
if (entries.length > 0) {
|
|
5566
|
+
w = Math.max(w, lb.width + 32 * 2);
|
|
5567
|
+
h = lb.height;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
w = round15(w);
|
|
5571
|
+
h = round15(h);
|
|
5572
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" role="img" aria-label="${xmlEscape(svgLabels.ariaLabel)}">` + parts.join("") + legendSvg + `</svg>`;
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5575
|
+
// src/prisma/render.ts
|
|
5576
|
+
function prismaSvg(input, opts = {}) {
|
|
5577
|
+
const layout = computePrismaLayout(input, opts);
|
|
5578
|
+
const svg = prismaLayoutSvg(layout, opts);
|
|
5579
|
+
return { svg, layout };
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
// src/uml/types.ts
|
|
5583
|
+
var UML_VISIBILITIES = ["public", "private", "protected", "package"];
|
|
5584
|
+
var UML_VIS_GLYPH = {
|
|
5585
|
+
public: "+",
|
|
5586
|
+
private: "-",
|
|
5587
|
+
protected: "#",
|
|
5588
|
+
package: "~"
|
|
5589
|
+
};
|
|
5590
|
+
var UML_RELATIONSHIPS = [
|
|
5591
|
+
"association",
|
|
5592
|
+
"directed-association",
|
|
5593
|
+
"aggregation",
|
|
5594
|
+
"composition",
|
|
5595
|
+
"generalization",
|
|
5596
|
+
"realization",
|
|
5597
|
+
"dependency"
|
|
5598
|
+
];
|
|
5599
|
+
var RELATION_GLYPHS = {
|
|
5600
|
+
association: { sourceEnd: "none", targetEnd: "none", line: "solid" },
|
|
5601
|
+
"directed-association": { sourceEnd: "none", targetEnd: "open-arrow", line: "solid" },
|
|
5602
|
+
aggregation: { sourceEnd: "hollow-diamond", targetEnd: "none", line: "solid" },
|
|
5603
|
+
composition: { sourceEnd: "solid-diamond", targetEnd: "none", line: "solid" },
|
|
5604
|
+
generalization: { sourceEnd: "none", targetEnd: "hollow-triangle", line: "solid" },
|
|
5605
|
+
realization: { sourceEnd: "none", targetEnd: "hollow-triangle", line: "dashed" },
|
|
5606
|
+
dependency: { sourceEnd: "none", targetEnd: "open-arrow", line: "dashed" }
|
|
5607
|
+
};
|
|
5608
|
+
|
|
5609
|
+
// src/uml/validate.ts
|
|
5610
|
+
var UmlValidationError = class extends Error {
|
|
5611
|
+
issues;
|
|
5612
|
+
constructor(issues) {
|
|
5613
|
+
super(`invalid UML class diagram: ${issues.map((i) => i.message).join("; ")}`);
|
|
5614
|
+
this.name = "UmlValidationError";
|
|
5615
|
+
this.issues = issues;
|
|
5616
|
+
}
|
|
5617
|
+
};
|
|
5618
|
+
function sortIssues6(issues) {
|
|
5619
|
+
const unique = /* @__PURE__ */ new Map();
|
|
5620
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
5621
|
+
return [...unique.values()].sort(
|
|
5622
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
5623
|
+
);
|
|
5624
|
+
}
|
|
5625
|
+
var GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
|
|
5626
|
+
"duplicate-id",
|
|
5627
|
+
"unknown-endpoint"
|
|
5628
|
+
]);
|
|
5629
|
+
var INHERITANCE_KINDS = /* @__PURE__ */ new Set([
|
|
5630
|
+
"generalization",
|
|
5631
|
+
"realization"
|
|
5632
|
+
]);
|
|
5633
|
+
function umlIssues(input) {
|
|
5634
|
+
if (input.classes.length === 0 && input.relationships.length === 0) return [];
|
|
5635
|
+
const issues = [];
|
|
5636
|
+
const push = (code, message) => {
|
|
5637
|
+
issues.push({ code, message });
|
|
5638
|
+
};
|
|
5639
|
+
const classById = /* @__PURE__ */ new Map();
|
|
5640
|
+
const dupClassIds = /* @__PURE__ */ new Set();
|
|
5641
|
+
for (const c of input.classes) {
|
|
5642
|
+
if (classById.has(c.id)) dupClassIds.add(c.id);
|
|
5643
|
+
else classById.set(c.id, c);
|
|
5644
|
+
}
|
|
5645
|
+
for (const id of [...dupClassIds].sort((a, b) => a - b)) {
|
|
5646
|
+
push("duplicate-id", `duplicate class id ${id}`);
|
|
5647
|
+
}
|
|
5648
|
+
const relById = /* @__PURE__ */ new Map();
|
|
5649
|
+
const dupRelIds = /* @__PURE__ */ new Set();
|
|
5650
|
+
for (const r2 of input.relationships) {
|
|
5651
|
+
if (relById.has(r2.id)) dupRelIds.add(r2.id);
|
|
5652
|
+
else relById.set(r2.id, r2);
|
|
5653
|
+
}
|
|
5654
|
+
for (const id of [...dupRelIds].sort((a, b) => a - b)) {
|
|
5655
|
+
push("duplicate-id", `duplicate relationship id ${id}`);
|
|
5656
|
+
}
|
|
5657
|
+
if (issues.length > 0) return sortIssues6(issues);
|
|
5658
|
+
const rels = [...relById.values()].sort((a, b) => a.id - b.id);
|
|
5659
|
+
for (const r2 of rels) {
|
|
5660
|
+
if (!classById.has(r2.sourceId)) {
|
|
5661
|
+
push("unknown-endpoint", `relationship ${r2.id} sourceId ${r2.sourceId} is not a declared class`);
|
|
5662
|
+
}
|
|
5663
|
+
if (!classById.has(r2.targetId)) {
|
|
5664
|
+
push("unknown-endpoint", `relationship ${r2.id} targetId ${r2.targetId} is not a declared class`);
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
|
|
5668
|
+
if (!Number.isInteger(c.col) || c.col < 0) {
|
|
5669
|
+
push("negative-cell", `class ${c.id} col ${c.col} must be a non-negative integer`);
|
|
5670
|
+
}
|
|
5671
|
+
if (!Number.isInteger(c.row) || c.row < 0) {
|
|
5672
|
+
push("negative-cell", `class ${c.id} row ${c.row} must be a non-negative integer`);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
const cellKey = (c) => `${c.col},${c.row}`;
|
|
5676
|
+
const cellToClasses = /* @__PURE__ */ new Map();
|
|
5677
|
+
for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
|
|
5678
|
+
const key = cellKey(c);
|
|
5679
|
+
const arr = cellToClasses.get(key) ?? [];
|
|
5680
|
+
arr.push(c.id);
|
|
5681
|
+
cellToClasses.set(key, arr);
|
|
5682
|
+
}
|
|
5683
|
+
for (const [key, ids] of [...cellToClasses.entries()].sort()) {
|
|
5684
|
+
if (ids.length > 1) {
|
|
5685
|
+
push(
|
|
5686
|
+
"cell-collision",
|
|
5687
|
+
`classes ${ids.sort((a, b) => a - b).join(", ")} all declare cell (${key})`
|
|
5688
|
+
);
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
for (const r2 of rels) {
|
|
5692
|
+
if ((r2.kind === "generalization" || r2.kind === "realization") && r2.sourceId === r2.targetId) {
|
|
5693
|
+
push(
|
|
5694
|
+
"self-generalization",
|
|
5695
|
+
`relationship ${r2.id} is a ${r2.kind} from class ${r2.sourceId} to itself`
|
|
5696
|
+
);
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
|
|
5700
|
+
const inheritanceChildren = /* @__PURE__ */ new Map();
|
|
5701
|
+
for (const r2 of rels) {
|
|
5702
|
+
if (!INHERITANCE_KINDS.has(r2.kind)) continue;
|
|
5703
|
+
if (r2.sourceId === r2.targetId) continue;
|
|
5704
|
+
const arr = inheritanceChildren.get(r2.sourceId) ?? [];
|
|
5705
|
+
arr.push(r2.targetId);
|
|
5706
|
+
inheritanceChildren.set(r2.sourceId, arr);
|
|
5707
|
+
}
|
|
5708
|
+
for (const arr of inheritanceChildren.values()) arr.sort((a, b) => a - b);
|
|
5709
|
+
const color = /* @__PURE__ */ new Map();
|
|
5710
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
5711
|
+
const dfs = (start) => {
|
|
5712
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
5713
|
+
color.set(start, 1);
|
|
5714
|
+
while (stack.length > 0) {
|
|
5715
|
+
const frame = stack[stack.length - 1];
|
|
5716
|
+
const children = inheritanceChildren.get(frame.id) ?? [];
|
|
5717
|
+
if (frame.nextChild >= children.length) {
|
|
5718
|
+
color.set(frame.id, 2);
|
|
5719
|
+
stack.pop();
|
|
5720
|
+
continue;
|
|
5721
|
+
}
|
|
5722
|
+
const child = children[frame.nextChild];
|
|
5723
|
+
frame.nextChild += 1;
|
|
5724
|
+
const c = color.get(child) ?? 0;
|
|
5725
|
+
if (c === 1) {
|
|
5726
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
5727
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
5728
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
5729
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
5730
|
+
const key = rotated.join(">");
|
|
5731
|
+
if (!seenCycles.has(key)) {
|
|
5732
|
+
seenCycles.add(key);
|
|
5733
|
+
push(
|
|
5734
|
+
"generalization-cycle",
|
|
5735
|
+
`generalization cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`
|
|
5736
|
+
);
|
|
5737
|
+
}
|
|
5738
|
+
} else if (c === 0) {
|
|
5739
|
+
color.set(child, 1);
|
|
5740
|
+
stack.push({ id: child, nextChild: 0 });
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
};
|
|
5744
|
+
for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
|
|
5745
|
+
if ((color.get(c.id) ?? 0) === 0) dfs(c.id);
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
return sortIssues6(issues);
|
|
5749
|
+
}
|
|
5750
|
+
function validateUml(input) {
|
|
5751
|
+
const issues = umlIssues(input);
|
|
5752
|
+
if (issues.length > 0) throw new UmlValidationError(issues);
|
|
5753
|
+
}
|
|
5754
|
+
function sideCapacity(boxH, rowGap, pitch, glyphHalf) {
|
|
5755
|
+
const clear = boxH / 2 + rowGap / 2 - glyphHalf;
|
|
5756
|
+
const k = Math.max(0, Math.floor(clear / pitch));
|
|
5757
|
+
return 2 * k + 1;
|
|
5758
|
+
}
|
|
5759
|
+
function sideCapacityIssue(classId, side, count, capacity) {
|
|
5760
|
+
return {
|
|
5761
|
+
code: "too-many-side-edges",
|
|
5762
|
+
message: `class ${classId} has ${count} edges on its ${side} side \u2014 at most ${capacity} fit without a port stub spilling through a neighbouring box`
|
|
5763
|
+
};
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
// src/uml/labels.ts
|
|
5767
|
+
var UML_TITLE_LABELS_EN = {
|
|
5768
|
+
kinds: {
|
|
5769
|
+
association: "association",
|
|
5770
|
+
"directed-association": "directed association",
|
|
5771
|
+
aggregation: "aggregation",
|
|
5772
|
+
composition: "composition",
|
|
5773
|
+
generalization: "generalization",
|
|
5774
|
+
realization: "realization",
|
|
5775
|
+
dependency: "dependency"
|
|
5776
|
+
},
|
|
5777
|
+
visibilities: {
|
|
5778
|
+
public: "public",
|
|
5779
|
+
private: "private",
|
|
5780
|
+
protected: "protected",
|
|
5781
|
+
package: "package"
|
|
5782
|
+
}
|
|
5783
|
+
};
|
|
5784
|
+
var UML_SVG_LABELS_EN = {
|
|
5785
|
+
legend: {
|
|
5786
|
+
association: "Association",
|
|
5787
|
+
"directed-association": "Directed association",
|
|
5788
|
+
aggregation: "Aggregation",
|
|
5789
|
+
composition: "Composition",
|
|
5790
|
+
generalization: "Generalization",
|
|
5791
|
+
realization: "Realization",
|
|
5792
|
+
dependency: "Dependency"
|
|
5793
|
+
},
|
|
5794
|
+
ariaLabel: "UML class diagram"
|
|
5795
|
+
};
|
|
5796
|
+
|
|
5797
|
+
// src/uml/layout.ts
|
|
5798
|
+
var UML_PADDING = 32;
|
|
5799
|
+
var UML_COL_GAP = 64;
|
|
5800
|
+
var UML_ROW_GAP = 56;
|
|
5801
|
+
var UML_BOX_PAD_X = 10;
|
|
5802
|
+
var UML_BOX_PAD_Y = 8;
|
|
5803
|
+
var UML_LINE_H = 15;
|
|
5804
|
+
var UML_MIN_BOX_W = 120;
|
|
5805
|
+
var UML_NAME_FONT = 12;
|
|
5806
|
+
var UML_FEAT_FONT = 11;
|
|
5807
|
+
var UML_SEP_PAD = 4;
|
|
5808
|
+
var UML_PORT_PITCH = 12;
|
|
5809
|
+
var UML_SELF_LOOP_W = 28;
|
|
5810
|
+
var UML_LABEL_FONT = 10;
|
|
5811
|
+
var UML_ASSOC_ROUTE_BASE = 1e6;
|
|
5812
|
+
var UML_GLYPH_BASE = 2e6;
|
|
5813
|
+
var UML_SELF_LOOP_BASE = 3e6;
|
|
5814
|
+
var MIN_TERMINAL_STUB = 4;
|
|
5815
|
+
var round16 = (n) => Math.round(n * 100) / 100;
|
|
5816
|
+
function featureLine(f) {
|
|
5817
|
+
const prefix = f.visibility !== null ? UML_VIS_GLYPH[f.visibility] + " " : "";
|
|
5818
|
+
return prefix + f.text;
|
|
5819
|
+
}
|
|
5820
|
+
function measureClass(cls) {
|
|
5821
|
+
const nameLines = [];
|
|
5822
|
+
if (cls.stereotype !== null) nameLines.push(cls.stereotype);
|
|
5823
|
+
nameLines.push(cls.name);
|
|
5824
|
+
const attrLines = cls.attributes.map(featureLine);
|
|
5825
|
+
const opLines = cls.operations.map(featureLine);
|
|
5826
|
+
return measureCompartmentBox(
|
|
5827
|
+
[
|
|
5828
|
+
{ lines: nameLines, font: UML_NAME_FONT },
|
|
5829
|
+
{ lines: attrLines, font: UML_FEAT_FONT },
|
|
5830
|
+
{ lines: opLines, font: UML_FEAT_FONT }
|
|
5831
|
+
],
|
|
5832
|
+
{
|
|
5833
|
+
padX: UML_BOX_PAD_X,
|
|
5834
|
+
padY: UML_BOX_PAD_Y,
|
|
5835
|
+
lineH: UML_LINE_H,
|
|
5836
|
+
minW: UML_MIN_BOX_W}
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
function boxEdges(node) {
|
|
5840
|
+
const left = round16(node.cx - node.boxW / 2);
|
|
5841
|
+
const right = round16(left + node.boxW);
|
|
5842
|
+
const top = round16(node.cy - node.boxH / 2);
|
|
5843
|
+
const bottom = round16(top + node.boxH);
|
|
5844
|
+
return { left, right, top, bottom };
|
|
5845
|
+
}
|
|
5846
|
+
function glyphInsetForKind(gk) {
|
|
5847
|
+
switch (gk) {
|
|
5848
|
+
case "none":
|
|
5849
|
+
return 0;
|
|
5850
|
+
case "open-arrow":
|
|
5851
|
+
return glyphInset("right", "arrow");
|
|
5852
|
+
case "hollow-triangle":
|
|
5853
|
+
return glyphInset("right", "triangle");
|
|
5854
|
+
case "hollow-diamond":
|
|
5855
|
+
case "solid-diamond":
|
|
5856
|
+
return glyphInset("right", "diamond");
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
var MAX_GLYPH_HALF = GLYPH_TRI_HALF;
|
|
5860
|
+
var GLYPH_SLOT_PITCH = Math.max(UML_PORT_PITCH, MAX_GLYPH_HALF * 2 + 2);
|
|
5861
|
+
function portOffset(slotIndex, slotSize) {
|
|
5862
|
+
if (slotIndex === 0) return 0;
|
|
5863
|
+
const sign = slotIndex % 2 === 1 ? 1 : -1;
|
|
5864
|
+
const dist = Math.ceil(slotIndex / 2);
|
|
5865
|
+
return sign * dist * slotSize;
|
|
5866
|
+
}
|
|
5867
|
+
function sidePort(node, side, dy) {
|
|
5868
|
+
const { left, right } = boxEdges(node);
|
|
5869
|
+
const x = side === "right" ? right : left;
|
|
5870
|
+
return { x, y: round16(node.cy + dy) };
|
|
5871
|
+
}
|
|
5872
|
+
function vGutterLeft(colIdx, colX, grid) {
|
|
5873
|
+
return round16(colX[colIdx] + grid.colW[colIdx]);
|
|
5874
|
+
}
|
|
5875
|
+
var V_GUTTER_MARGIN = 6;
|
|
5876
|
+
function horizontalGutterY(rowIdx, rowY, grid) {
|
|
5877
|
+
return round16(rowY[rowIdx] + grid.rowH[rowIdx] + UML_ROW_GAP / 2);
|
|
5878
|
+
}
|
|
5879
|
+
function moveAlongDir(p, dir, dist) {
|
|
5880
|
+
switch (dir) {
|
|
5881
|
+
case "right":
|
|
5882
|
+
return { x: round16(p.x + dist), y: p.y };
|
|
5883
|
+
case "left":
|
|
5884
|
+
return { x: round16(p.x - dist), y: p.y };
|
|
5885
|
+
case "down":
|
|
5886
|
+
return { x: p.x, y: round16(p.y + dist) };
|
|
5887
|
+
case "up":
|
|
5888
|
+
return { x: p.x, y: round16(p.y - dist) };
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
function simplifyWaypoints(pts) {
|
|
5892
|
+
const dedup = [pts[0]];
|
|
5893
|
+
for (let i = 1; i < pts.length; i++) {
|
|
5894
|
+
const prev = dedup[dedup.length - 1];
|
|
5895
|
+
const cur = pts[i];
|
|
5896
|
+
if (Math.abs(cur.x - prev.x) > 1e-6 || Math.abs(cur.y - prev.y) > 1e-6) dedup.push(cur);
|
|
5897
|
+
}
|
|
5898
|
+
if (dedup.length <= 2) return dedup;
|
|
5899
|
+
const out = [dedup[0]];
|
|
5900
|
+
for (let i = 1; i < dedup.length - 1; i++) {
|
|
5901
|
+
const a = out[out.length - 1];
|
|
5902
|
+
const b = dedup[i];
|
|
5903
|
+
const c = dedup[i + 1];
|
|
5904
|
+
const abH = Math.abs(a.y - b.y) <= 1e-6;
|
|
5905
|
+
const bcH = Math.abs(b.y - c.y) <= 1e-6;
|
|
5906
|
+
const abV = Math.abs(a.x - b.x) <= 1e-6;
|
|
5907
|
+
const bcV = Math.abs(b.x - c.x) <= 1e-6;
|
|
5908
|
+
if (abH && bcH || abV && bcV) continue;
|
|
5909
|
+
out.push(b);
|
|
5910
|
+
}
|
|
5911
|
+
out.push(dedup[dedup.length - 1]);
|
|
5912
|
+
return out;
|
|
5913
|
+
}
|
|
5914
|
+
function chooseSides(srcNode, tgtNode, maxCol) {
|
|
5915
|
+
const sc = srcNode.col;
|
|
5916
|
+
const tc = tgtNode.col;
|
|
5917
|
+
if (tc > sc) return { srcSide: "right", tgtSide: "left", srcVCol: sc, tgtVCol: tc - 1 };
|
|
5918
|
+
if (tc < sc) return { srcSide: "left", tgtSide: "right", srcVCol: sc - 1, tgtVCol: tc };
|
|
5919
|
+
if (sc < maxCol) return { srcSide: "right", tgtSide: "right", srcVCol: sc, tgtVCol: sc };
|
|
5920
|
+
if (sc > 0) return { srcSide: "left", tgtSide: "left", srcVCol: sc - 1, tgtVCol: sc - 1 };
|
|
5921
|
+
return { srcSide: "right", tgtSide: "right", srcVCol: sc, tgtVCol: sc };
|
|
5922
|
+
}
|
|
5923
|
+
function hBaseY(plan, rowY, grid) {
|
|
5924
|
+
if (plan.hKind === "pad-top") return round16(UML_PADDING / 2);
|
|
5925
|
+
return horizontalGutterY(plan.hRow, rowY, grid);
|
|
5926
|
+
}
|
|
5927
|
+
function hBandBounds(plan, rowY, grid) {
|
|
5928
|
+
if (plan.hKind === "pad-top") return { lo: 0, hi: round16(rowY[0] ?? UML_PADDING) };
|
|
5929
|
+
const lo = round16(rowY[plan.hRow] + grid.rowH[plan.hRow]);
|
|
5930
|
+
const hi = round16(rowY[plan.hRow + 1]);
|
|
5931
|
+
return { lo, hi };
|
|
5932
|
+
}
|
|
5933
|
+
var LANE_EPS = 0.5;
|
|
5934
|
+
var X_OVERLAP_EPS = 1e-4;
|
|
5935
|
+
function allocateHorizontal(runs, base, usableHalf) {
|
|
5936
|
+
const floating = runs.filter((r2) => r2.y === null);
|
|
5937
|
+
if (floating.length === 0) return;
|
|
5938
|
+
const pinned = runs.filter((r2) => r2.y !== null);
|
|
5939
|
+
const trackCount = floating.length + pinned.length + 1;
|
|
5940
|
+
const candidates = trackCount <= 1 || usableHalf <= 0 ? [base] : Array.from(
|
|
5941
|
+
{ length: trackCount },
|
|
5942
|
+
(_, k) => round16(base - usableHalf + (k + 0.5) * (2 * usableHalf) / trackCount)
|
|
5943
|
+
);
|
|
5944
|
+
const placed = pinned.map((r2) => ({ lo: r2.lo, hi: r2.hi, y: r2.y }));
|
|
5945
|
+
const ordered = [...floating].sort((a, b) => a.lo - b.lo || a.hi - b.hi);
|
|
5946
|
+
for (const run of ordered) {
|
|
5947
|
+
let chosen = candidates[0];
|
|
5948
|
+
for (const cy of candidates) {
|
|
5949
|
+
const collides = placed.some(
|
|
5950
|
+
(q) => Math.abs(q.y - cy) <= LANE_EPS && Math.min(run.hi, q.hi) - Math.max(run.lo, q.lo) > X_OVERLAP_EPS
|
|
5951
|
+
);
|
|
5952
|
+
if (!collides) {
|
|
5953
|
+
chosen = cy;
|
|
5954
|
+
break;
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
run.set(chosen);
|
|
5958
|
+
placed.push({ lo: run.lo, hi: run.hi, y: chosen });
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
function applyGlyphInsets(pts, srcInset, tgtInset) {
|
|
5962
|
+
if (pts.length < 2) return pts;
|
|
5963
|
+
const out = pts.map((p) => ({ ...p }));
|
|
5964
|
+
if (srcInset > 0) {
|
|
5965
|
+
const p0 = out[0];
|
|
5966
|
+
const p1 = out[1];
|
|
5967
|
+
const dir = p1.x >= p0.x ? "right" : "left";
|
|
5968
|
+
out[0] = moveAlongDir(p0, dir, srcInset);
|
|
5969
|
+
}
|
|
5970
|
+
if (tgtInset > 0) {
|
|
5971
|
+
const pn = out[out.length - 1];
|
|
5972
|
+
const pm = out[out.length - 2];
|
|
5973
|
+
const dir = pm.x >= pn.x ? "right" : "left";
|
|
5974
|
+
out[out.length - 1] = moveAlongDir(pn, dir, tgtInset);
|
|
5975
|
+
}
|
|
5976
|
+
return out;
|
|
5977
|
+
}
|
|
5978
|
+
function reserveTextBox(relId, slot, text, anchorX, anchorY, anchor) {
|
|
5979
|
+
const w = round16(estimateTextWidth(text, UML_LABEL_FONT));
|
|
5980
|
+
const h = round16(UML_LABEL_FONT * 1.2);
|
|
5981
|
+
const x = anchor === "middle" ? round16(anchorX - w / 2) : round16(anchorX);
|
|
5982
|
+
const y = round16(anchorY);
|
|
5983
|
+
return { relId, slot, text, x, y, w, h, anchor };
|
|
5984
|
+
}
|
|
5985
|
+
function computeUmlLayout(input, labels = UML_TITLE_LABELS_EN) {
|
|
5986
|
+
validateUml(input);
|
|
5987
|
+
const relTitle = (rel) => rel.title ?? labels.kinds[rel.kind];
|
|
5988
|
+
if (input.classes.length === 0) {
|
|
5989
|
+
return {
|
|
5990
|
+
nodes: [],
|
|
5991
|
+
elements: [],
|
|
5992
|
+
textBoxes: [],
|
|
5993
|
+
canvasW: UML_PADDING * 2,
|
|
5994
|
+
canvasH: UML_PADDING * 2,
|
|
5995
|
+
grid: { cellX: [], cellY: [], colW: [], rowH: [] },
|
|
5996
|
+
colX: [],
|
|
5997
|
+
rowY: []
|
|
5998
|
+
};
|
|
5999
|
+
}
|
|
6000
|
+
const classes = [...input.classes].sort((a, b) => a.id - b.id);
|
|
6001
|
+
const rels = [...input.relationships].sort((a, b) => a.id - b.id);
|
|
6002
|
+
const metricsMap = /* @__PURE__ */ new Map();
|
|
6003
|
+
for (const cls of classes) metricsMap.set(cls.id, measureClass(cls));
|
|
6004
|
+
const gridCells = classes.map((cls) => ({
|
|
6005
|
+
col: cls.col,
|
|
6006
|
+
row: cls.row,
|
|
6007
|
+
w: metricsMap.get(cls.id).boxW,
|
|
6008
|
+
h: metricsMap.get(cls.id).boxH
|
|
6009
|
+
}));
|
|
6010
|
+
const grid = packGrid(gridCells, { colGap: UML_COL_GAP, rowGap: UML_ROW_GAP });
|
|
6011
|
+
const colX = [];
|
|
6012
|
+
{
|
|
6013
|
+
let x = UML_PADDING;
|
|
6014
|
+
for (let c = 0; c < grid.colW.length; c++) {
|
|
6015
|
+
colX.push(round16(x));
|
|
6016
|
+
x = round16(x + grid.colW[c] + UML_COL_GAP);
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
const rowY = [];
|
|
6020
|
+
{
|
|
6021
|
+
let y = UML_PADDING;
|
|
6022
|
+
for (let r2 = 0; r2 < grid.rowH.length; r2++) {
|
|
6023
|
+
rowY.push(round16(y));
|
|
6024
|
+
y = round16(y + grid.rowH[r2] + UML_ROW_GAP);
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
const nodeByClassId = /* @__PURE__ */ new Map();
|
|
6028
|
+
const nodes = [];
|
|
6029
|
+
for (const cls of classes) {
|
|
6030
|
+
const metrics = metricsMap.get(cls.id);
|
|
6031
|
+
const cx = round16(colX[cls.col] + grid.colW[cls.col] / 2);
|
|
6032
|
+
const cy = round16(rowY[cls.row] + grid.rowH[cls.row] / 2);
|
|
6033
|
+
const title = cls.title ?? (cls.stereotype !== null ? `\xAB${cls.stereotype}\xBB ${cls.name}` : cls.name);
|
|
6034
|
+
const node = {
|
|
6035
|
+
classId: cls.id,
|
|
6036
|
+
cx,
|
|
6037
|
+
cy,
|
|
6038
|
+
boxW: metrics.boxW,
|
|
6039
|
+
boxH: metrics.boxH,
|
|
6040
|
+
metrics,
|
|
6041
|
+
title,
|
|
6042
|
+
col: cls.col,
|
|
6043
|
+
row: cls.row
|
|
6044
|
+
};
|
|
6045
|
+
nodes.push(node);
|
|
6046
|
+
nodeByClassId.set(cls.id, node);
|
|
6047
|
+
}
|
|
6048
|
+
const maxCol = grid.colW.length - 1;
|
|
6049
|
+
const maxRow = grid.rowH.length - 1;
|
|
6050
|
+
const sideCount = /* @__PURE__ */ new Map();
|
|
6051
|
+
const bumpSide = (classId, side) => {
|
|
6052
|
+
const key = `${classId}:${side}`;
|
|
6053
|
+
sideCount.set(key, (sideCount.get(key) ?? 0) + 1);
|
|
6054
|
+
};
|
|
6055
|
+
for (const rel of rels) {
|
|
6056
|
+
const srcNode = nodeByClassId.get(rel.sourceId);
|
|
6057
|
+
const tgtNode = nodeByClassId.get(rel.targetId);
|
|
6058
|
+
if (!srcNode || !tgtNode) continue;
|
|
6059
|
+
if (rel.sourceId === rel.targetId) {
|
|
6060
|
+
bumpSide(srcNode.classId, "right");
|
|
6061
|
+
bumpSide(srcNode.classId, "right");
|
|
6062
|
+
continue;
|
|
6063
|
+
}
|
|
6064
|
+
const { srcSide, tgtSide } = chooseSides(srcNode, tgtNode, maxCol);
|
|
6065
|
+
bumpSide(srcNode.classId, srcSide);
|
|
6066
|
+
bumpSide(tgtNode.classId, tgtSide);
|
|
6067
|
+
}
|
|
6068
|
+
{
|
|
6069
|
+
const capIssues = [];
|
|
6070
|
+
for (const node of nodes) {
|
|
6071
|
+
const cap = sideCapacity(node.boxH, UML_ROW_GAP, GLYPH_SLOT_PITCH, MAX_GLYPH_HALF);
|
|
6072
|
+
for (const side of ["left", "right"]) {
|
|
6073
|
+
const count = sideCount.get(`${node.classId}:${side}`) ?? 0;
|
|
6074
|
+
if (count > cap) capIssues.push(sideCapacityIssue(node.classId, side, count, cap));
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
if (capIssues.length > 0) {
|
|
6078
|
+
throw new UmlValidationError(capIssues);
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
const sidePortSlot = /* @__PURE__ */ new Map();
|
|
6082
|
+
const nextSidePort = (classId, side) => {
|
|
6083
|
+
const key = `${classId}:${side}`;
|
|
6084
|
+
const idx = sidePortSlot.get(key) ?? 0;
|
|
6085
|
+
sidePortSlot.set(key, idx + 1);
|
|
6086
|
+
return idx;
|
|
6087
|
+
};
|
|
6088
|
+
const elements = [];
|
|
6089
|
+
const textBoxes = [];
|
|
6090
|
+
const plans = [];
|
|
6091
|
+
for (const rel of rels) {
|
|
6092
|
+
const srcNode = nodeByClassId.get(rel.sourceId);
|
|
6093
|
+
const tgtNode = nodeByClassId.get(rel.targetId);
|
|
6094
|
+
if (!srcNode || !tgtNode) continue;
|
|
6095
|
+
const glyphs = RELATION_GLYPHS[rel.kind];
|
|
6096
|
+
const srcInset = glyphInsetForKind(glyphs.sourceEnd);
|
|
6097
|
+
const tgtInset = glyphInsetForKind(glyphs.targetEnd);
|
|
6098
|
+
if (rel.sourceId === rel.targetId) {
|
|
6099
|
+
const vCol = srcNode.col;
|
|
6100
|
+
const armDy1 = portOffset(nextSidePort(srcNode.classId, "right"), GLYPH_SLOT_PITCH);
|
|
6101
|
+
const armDy2 = portOffset(nextSidePort(srcNode.classId, "right"), GLYPH_SLOT_PITCH);
|
|
6102
|
+
plans.push({
|
|
6103
|
+
rel,
|
|
6104
|
+
srcNode,
|
|
6105
|
+
tgtNode,
|
|
6106
|
+
srcSide: "right",
|
|
6107
|
+
tgtSide: "right",
|
|
6108
|
+
srcVCol: vCol,
|
|
6109
|
+
tgtVCol: vCol,
|
|
6110
|
+
srcInset,
|
|
6111
|
+
tgtInset,
|
|
6112
|
+
srcPortY: round16(srcNode.cy + armDy1),
|
|
6113
|
+
tgtPortY: round16(srcNode.cy + armDy2),
|
|
6114
|
+
hKind: "row",
|
|
6115
|
+
hRow: 0,
|
|
6116
|
+
// unused for self-loops (no H-leg)
|
|
6117
|
+
isSelfLoop: true,
|
|
6118
|
+
srcVx: 0,
|
|
6119
|
+
tgtVx: 0,
|
|
6120
|
+
hY: 0
|
|
6121
|
+
});
|
|
6122
|
+
continue;
|
|
6123
|
+
}
|
|
6124
|
+
const { srcSide, tgtSide, srcVCol, tgtVCol } = chooseSides(srcNode, tgtNode, maxCol);
|
|
6125
|
+
const srcPortDy = portOffset(nextSidePort(srcNode.classId, srcSide), GLYPH_SLOT_PITCH);
|
|
6126
|
+
const tgtPortDy = portOffset(nextSidePort(tgtNode.classId, tgtSide), GLYPH_SLOT_PITCH);
|
|
6127
|
+
const sr = srcNode.row;
|
|
6128
|
+
const tr = tgtNode.row;
|
|
6129
|
+
let hKind;
|
|
6130
|
+
let hRow;
|
|
6131
|
+
if (sr !== tr) {
|
|
6132
|
+
hKind = "row";
|
|
6133
|
+
hRow = Math.min(sr, tr);
|
|
6134
|
+
} else if (maxRow >= 1) {
|
|
6135
|
+
hKind = "row";
|
|
6136
|
+
hRow = sr < maxRow ? sr : sr - 1;
|
|
6137
|
+
} else {
|
|
6138
|
+
hKind = "pad-top";
|
|
6139
|
+
hRow = -1;
|
|
6140
|
+
}
|
|
6141
|
+
plans.push({
|
|
6142
|
+
rel,
|
|
6143
|
+
srcNode,
|
|
6144
|
+
tgtNode,
|
|
6145
|
+
srcSide,
|
|
6146
|
+
tgtSide,
|
|
6147
|
+
srcVCol,
|
|
6148
|
+
tgtVCol,
|
|
6149
|
+
srcInset,
|
|
6150
|
+
tgtInset,
|
|
6151
|
+
srcPortY: round16(srcNode.cy + srcPortDy),
|
|
6152
|
+
tgtPortY: round16(tgtNode.cy + tgtPortDy),
|
|
6153
|
+
hKind,
|
|
6154
|
+
hRow,
|
|
6155
|
+
isSelfLoop: false,
|
|
6156
|
+
srcVx: 0,
|
|
6157
|
+
tgtVx: 0,
|
|
6158
|
+
hY: 0
|
|
6159
|
+
});
|
|
6160
|
+
}
|
|
6161
|
+
const hHalfBand = (p) => p.hKind === "pad-top" ? round16(UML_PADDING / 2) : round16(UML_ROW_GAP / 2);
|
|
6162
|
+
const vByKey = /* @__PURE__ */ new Map();
|
|
6163
|
+
const pushV = (col, side, portY, hBase, half, inset, set) => {
|
|
6164
|
+
const lo = Math.min(portY, round16(hBase - half));
|
|
6165
|
+
const hi = Math.max(portY, round16(hBase + half));
|
|
6166
|
+
const key = `${col}:${side}`;
|
|
6167
|
+
const arr = vByKey.get(key) ?? [];
|
|
6168
|
+
arr.push({ lo, hi, inset, lane: 0, set });
|
|
6169
|
+
vByKey.set(key, arr);
|
|
6170
|
+
};
|
|
6171
|
+
for (const p of plans) {
|
|
6172
|
+
if (p.isSelfLoop) {
|
|
6173
|
+
const lo = Math.min(p.srcPortY, p.tgtPortY);
|
|
6174
|
+
const hi = Math.max(p.srcPortY, p.tgtPortY);
|
|
6175
|
+
const key = `${p.srcVCol}:right`;
|
|
6176
|
+
const arr = vByKey.get(key) ?? [];
|
|
6177
|
+
arr.push({ lo, hi, inset: Math.max(p.srcInset, p.tgtInset), lane: 0, set: (x) => {
|
|
6178
|
+
p.srcVx = x;
|
|
6179
|
+
p.tgtVx = x;
|
|
6180
|
+
} });
|
|
6181
|
+
vByKey.set(key, arr);
|
|
6182
|
+
continue;
|
|
6183
|
+
}
|
|
6184
|
+
if (p.srcVCol === p.tgtVCol && p.srcSide === p.tgtSide) {
|
|
6185
|
+
const lo = Math.min(p.srcPortY, p.tgtPortY);
|
|
6186
|
+
const hi = Math.max(p.srcPortY, p.tgtPortY);
|
|
6187
|
+
const key = `${p.srcVCol}:${p.srcSide}`;
|
|
6188
|
+
const arr = vByKey.get(key) ?? [];
|
|
6189
|
+
arr.push({ lo, hi, inset: Math.max(p.srcInset, p.tgtInset), lane: 0, set: (x) => {
|
|
6190
|
+
p.srcVx = x;
|
|
6191
|
+
p.tgtVx = x;
|
|
6192
|
+
} });
|
|
6193
|
+
vByKey.set(key, arr);
|
|
6194
|
+
continue;
|
|
6195
|
+
}
|
|
6196
|
+
const hBase = hBaseY(p, rowY, grid);
|
|
6197
|
+
const half = hHalfBand(p);
|
|
6198
|
+
pushV(p.srcVCol, p.srcSide, p.srcPortY, hBase, half, p.srcInset, (x) => {
|
|
6199
|
+
p.srcVx = x;
|
|
6200
|
+
});
|
|
6201
|
+
pushV(p.tgtVCol, p.tgtSide, p.tgtPortY, hBase, half, p.tgtInset, (x) => {
|
|
6202
|
+
p.tgtVx = x;
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
6205
|
+
for (const [key, items] of vByKey.entries()) {
|
|
6206
|
+
const side = key.endsWith(":right") ? "right" : "left";
|
|
6207
|
+
const col = Number(key.slice(0, key.lastIndexOf(":")));
|
|
6208
|
+
const laneCount = allocateLanes(
|
|
6209
|
+
items.map((it) => ({ lo: it.lo, hi: it.hi, set: (l) => {
|
|
6210
|
+
it.lane = l;
|
|
6211
|
+
} }))
|
|
6212
|
+
);
|
|
6213
|
+
const left = vGutterLeft(col, colX, grid);
|
|
6214
|
+
const right = round16(left + UML_COL_GAP);
|
|
6215
|
+
const center = round16((left + right) / 2);
|
|
6216
|
+
const maxBaseGap = Math.max(...items.map((it) => Math.max(V_GUTTER_MARGIN, it.inset + MIN_TERMINAL_STUB)));
|
|
6217
|
+
for (const it of items) {
|
|
6218
|
+
let x;
|
|
6219
|
+
if (side === "right") {
|
|
6220
|
+
const lo = left + maxBaseGap;
|
|
6221
|
+
const hi = center - 1;
|
|
6222
|
+
x = laneCount <= 1 ? lo : lo + it.lane * (hi - lo) / (laneCount - 1);
|
|
6223
|
+
} else {
|
|
6224
|
+
const hi = right - maxBaseGap;
|
|
6225
|
+
const lo = center + 1;
|
|
6226
|
+
x = laneCount <= 1 ? hi : hi - it.lane * (hi - lo) / (laneCount - 1);
|
|
6227
|
+
}
|
|
6228
|
+
it.set(round16(x));
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
const usableHalfOf = (kind) => Math.max(0, (kind === "pad-top" ? round16(UML_PADDING / 2) : round16(UML_ROW_GAP / 2)) - 3);
|
|
6232
|
+
const gutterKey = (p) => p.hKind === "pad-top" ? "pad-top" : `row:${p.hRow}`;
|
|
6233
|
+
const gutterMeta = /* @__PURE__ */ new Map();
|
|
6234
|
+
for (const p of plans) {
|
|
6235
|
+
if (p.isSelfLoop) continue;
|
|
6236
|
+
const key = gutterKey(p);
|
|
6237
|
+
if (!gutterMeta.has(key)) gutterMeta.set(key, { base: hBaseY(p, rowY, grid), usableHalf: usableHalfOf(p.hKind) });
|
|
6238
|
+
}
|
|
6239
|
+
const hRunsByKey = /* @__PURE__ */ new Map();
|
|
6240
|
+
const pushRun = (key, run) => {
|
|
6241
|
+
const arr = hRunsByKey.get(key) ?? [];
|
|
6242
|
+
arr.push(run);
|
|
6243
|
+
hRunsByKey.set(key, arr);
|
|
6244
|
+
};
|
|
6245
|
+
const pinStub = (yy, a, b) => {
|
|
6246
|
+
for (const [gk, meta] of gutterMeta.entries()) {
|
|
6247
|
+
if (yy >= meta.base - meta.usableHalf - LANE_EPS && yy <= meta.base + meta.usableHalf + LANE_EPS) {
|
|
6248
|
+
pushRun(gk, { lo: Math.min(a, b), hi: Math.max(a, b), y: yy, set: () => {
|
|
6249
|
+
} });
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
};
|
|
6253
|
+
for (const p of plans) {
|
|
6254
|
+
if (p.isSelfLoop) {
|
|
6255
|
+
const sideX = p.srcSide === "right" ? boxEdges(p.srcNode).right : boxEdges(p.srcNode).left;
|
|
6256
|
+
pinStub(p.srcPortY, sideX, p.srcVx);
|
|
6257
|
+
pinStub(p.tgtPortY, sideX, p.tgtVx);
|
|
6258
|
+
continue;
|
|
6259
|
+
}
|
|
6260
|
+
const key = gutterKey(p);
|
|
6261
|
+
if (Math.abs(p.srcVx - p.tgtVx) > LANE_EPS) {
|
|
6262
|
+
pushRun(key, { lo: Math.min(p.srcVx, p.tgtVx), hi: Math.max(p.srcVx, p.tgtVx), y: null, set: (y) => {
|
|
6263
|
+
p.hY = round16(y);
|
|
6264
|
+
} });
|
|
6265
|
+
} else {
|
|
6266
|
+
p.hY = p.srcPortY;
|
|
6267
|
+
}
|
|
6268
|
+
const srcPortX = p.srcSide === "right" ? boxEdges(p.srcNode).right : boxEdges(p.srcNode).left;
|
|
6269
|
+
const tgtPortX = p.tgtSide === "right" ? boxEdges(p.tgtNode).right : boxEdges(p.tgtNode).left;
|
|
6270
|
+
pinStub(p.srcPortY, srcPortX, p.srcVx);
|
|
6271
|
+
pinStub(p.tgtPortY, tgtPortX, p.tgtVx);
|
|
6272
|
+
}
|
|
6273
|
+
for (const [key, runs] of hRunsByKey.entries()) {
|
|
6274
|
+
const meta = gutterMeta.get(key);
|
|
6275
|
+
allocateHorizontal(runs, meta.base, meta.usableHalf);
|
|
6276
|
+
}
|
|
6277
|
+
for (const p of plans) {
|
|
6278
|
+
const srcPort = sidePort(p.srcNode, p.srcSide, p.srcPortY - p.srcNode.cy);
|
|
6279
|
+
const tgtPort = sidePort(p.tgtNode, p.tgtSide, p.tgtPortY - p.tgtNode.cy);
|
|
6280
|
+
if (p.isSelfLoop) {
|
|
6281
|
+
const raw2 = [
|
|
6282
|
+
srcPort,
|
|
6283
|
+
{ x: p.srcVx, y: srcPort.y },
|
|
6284
|
+
{ x: p.srcVx, y: tgtPort.y },
|
|
6285
|
+
tgtPort
|
|
6286
|
+
];
|
|
6287
|
+
const portPts2 = simplifyWaypoints(raw2);
|
|
6288
|
+
const pts2 = applyGlyphInsets(portPts2, p.srcInset, p.tgtInset);
|
|
6289
|
+
const edgeId2 = UML_SELF_LOOP_BASE + p.rel.id;
|
|
6290
|
+
elements.push({ edgeId: edgeId2, relId: p.rel.id, points: pts2, kind: "self-loop", title: relTitle(p.rel) });
|
|
6291
|
+
reserveSelfLoopText(p.rel, p, textBoxes);
|
|
6292
|
+
continue;
|
|
6293
|
+
}
|
|
6294
|
+
const raw = [
|
|
6295
|
+
srcPort,
|
|
6296
|
+
{ x: p.srcVx, y: srcPort.y },
|
|
6297
|
+
{ x: p.srcVx, y: p.hY },
|
|
6298
|
+
{ x: p.tgtVx, y: p.hY },
|
|
6299
|
+
{ x: p.tgtVx, y: tgtPort.y },
|
|
6300
|
+
tgtPort
|
|
6301
|
+
];
|
|
6302
|
+
const portPts = simplifyWaypoints(raw);
|
|
6303
|
+
const pts = applyGlyphInsets(portPts, p.srcInset, p.tgtInset);
|
|
6304
|
+
const edgeId = UML_ASSOC_ROUTE_BASE + p.rel.id;
|
|
6305
|
+
elements.push({ edgeId, relId: p.rel.id, points: pts, kind: "route", title: relTitle(p.rel) });
|
|
6306
|
+
reserveRouteText(p.rel, p, hBandBounds(p, rowY, grid), textBoxes);
|
|
6307
|
+
}
|
|
6308
|
+
let maxRight = 0;
|
|
6309
|
+
let maxBottom = 0;
|
|
6310
|
+
for (const node of nodes) {
|
|
6311
|
+
const { right, bottom } = boxEdges(node);
|
|
6312
|
+
if (right > maxRight) maxRight = right;
|
|
6313
|
+
if (bottom > maxBottom) maxBottom = bottom;
|
|
6314
|
+
}
|
|
6315
|
+
for (const el of elements) {
|
|
6316
|
+
for (const pt2 of el.points) {
|
|
6317
|
+
if (pt2.x > maxRight) maxRight = pt2.x;
|
|
6318
|
+
if (pt2.y > maxBottom) maxBottom = pt2.y;
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
for (const tb of textBoxes) {
|
|
6322
|
+
if (tb.x + tb.w > maxRight) maxRight = tb.x + tb.w;
|
|
6323
|
+
if (tb.y + tb.h > maxBottom) maxBottom = tb.y + tb.h;
|
|
6324
|
+
}
|
|
6325
|
+
const canvasW = round16(maxRight + UML_PADDING);
|
|
6326
|
+
const canvasH = round16(maxBottom + UML_PADDING);
|
|
6327
|
+
return { nodes, elements, textBoxes, canvasW, canvasH, grid, colX, rowY };
|
|
6328
|
+
}
|
|
6329
|
+
function reserveRouteText(rel, p, band, out) {
|
|
6330
|
+
const textH = round16(UML_LABEL_FONT * 1.2);
|
|
6331
|
+
const clampY = (y) => round16(Math.max(band.lo + 1, Math.min(y, band.hi - 1 - textH)));
|
|
6332
|
+
const above = clampY(p.hY - textH - 1);
|
|
6333
|
+
const below = clampY(p.hY + 1);
|
|
6334
|
+
const srcX = round16(p.srcVx + 3);
|
|
6335
|
+
const tgtX = round16(p.tgtVx + 3);
|
|
6336
|
+
if (rel.sourceMultiplicity !== null) {
|
|
6337
|
+
out.push(reserveTextBox(rel.id, "src-mult", rel.sourceMultiplicity, srcX, above, "start"));
|
|
6338
|
+
}
|
|
6339
|
+
if (rel.sourceRole !== null) {
|
|
6340
|
+
out.push(reserveTextBox(rel.id, "src-role", rel.sourceRole, srcX, below, "start"));
|
|
6341
|
+
}
|
|
6342
|
+
if (rel.targetMultiplicity !== null) {
|
|
6343
|
+
out.push(reserveTextBox(rel.id, "tgt-mult", rel.targetMultiplicity, tgtX, above, "start"));
|
|
6344
|
+
}
|
|
6345
|
+
if (rel.targetRole !== null) {
|
|
6346
|
+
out.push(reserveTextBox(rel.id, "tgt-role", rel.targetRole, tgtX, below, "start"));
|
|
6347
|
+
}
|
|
6348
|
+
if (rel.label !== null) {
|
|
6349
|
+
const midX = round16((p.srcVx + p.tgtVx) / 2);
|
|
6350
|
+
out.push(reserveTextBox(rel.id, "label", rel.label, midX, above, "middle"));
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
function reserveSelfLoopText(rel, p, out) {
|
|
6354
|
+
const x0 = round16(p.srcVx + 3);
|
|
6355
|
+
const srcY = p.srcPortY;
|
|
6356
|
+
const tgtY = p.tgtPortY;
|
|
6357
|
+
if (rel.sourceMultiplicity !== null) {
|
|
6358
|
+
out.push(reserveTextBox(rel.id, "src-mult", rel.sourceMultiplicity, x0, round16(srcY - UML_LABEL_FONT - 1), "start"));
|
|
6359
|
+
}
|
|
6360
|
+
if (rel.sourceRole !== null) {
|
|
6361
|
+
out.push(reserveTextBox(rel.id, "src-role", rel.sourceRole, x0, round16(srcY + 1), "start"));
|
|
6362
|
+
}
|
|
6363
|
+
if (rel.targetMultiplicity !== null) {
|
|
6364
|
+
out.push(reserveTextBox(rel.id, "tgt-mult", rel.targetMultiplicity, x0, round16(tgtY - UML_LABEL_FONT - 1), "start"));
|
|
6365
|
+
}
|
|
6366
|
+
if (rel.targetRole !== null) {
|
|
6367
|
+
out.push(reserveTextBox(rel.id, "tgt-role", rel.targetRole, x0, round16(tgtY + 1), "start"));
|
|
6368
|
+
}
|
|
6369
|
+
if (rel.label !== null) {
|
|
6370
|
+
out.push(reserveTextBox(rel.id, "label", rel.label, x0, round16((srcY + tgtY) / 2 - UML_LABEL_FONT / 2), "start"));
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
// src/uml/svg.ts
|
|
6375
|
+
var GLYPH_STROKE6 = "#52525b";
|
|
6376
|
+
var LABEL_FILL7 = "#3f3f46";
|
|
6377
|
+
var EDGE_INK8 = "#71717a";
|
|
6378
|
+
var round17 = (n) => Math.round(n * 100) / 100;
|
|
6379
|
+
function emitGlyph(gk, tip, dir) {
|
|
6380
|
+
switch (gk) {
|
|
6381
|
+
case "none":
|
|
6382
|
+
return "";
|
|
6383
|
+
case "open-arrow": {
|
|
6384
|
+
const pts = arrowOpenPoints(tip, dir);
|
|
6385
|
+
return `<polyline points="${pts}" fill="none" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
|
|
6386
|
+
}
|
|
6387
|
+
case "hollow-triangle": {
|
|
6388
|
+
const pts = trianglePoints(tip, dir);
|
|
6389
|
+
return `<polygon points="${pts}" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
|
|
6390
|
+
}
|
|
6391
|
+
case "hollow-diamond": {
|
|
6392
|
+
const pts = diamondPoints(tip, dir);
|
|
6393
|
+
return `<polygon points="${pts}" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
|
|
6394
|
+
}
|
|
6395
|
+
case "solid-diamond": {
|
|
6396
|
+
const pts = diamondPoints(tip, dir);
|
|
6397
|
+
return `<polygon points="${pts}" fill="${GLYPH_STROKE6}" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
function glyphDir(firstPt, secondPt) {
|
|
6402
|
+
const dx = secondPt.x - firstPt.x;
|
|
6403
|
+
const dy = secondPt.y - firstPt.y;
|
|
6404
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
6405
|
+
return dx >= 0 ? "left" : "right";
|
|
6406
|
+
}
|
|
6407
|
+
return dy >= 0 ? "up" : "down";
|
|
6408
|
+
}
|
|
6409
|
+
function featureSvg(f, x, y, font) {
|
|
6410
|
+
const prefix = f.visibility !== null ? UML_VIS_GLYPH[f.visibility] + " " : "";
|
|
6411
|
+
const text = prefix + f.text;
|
|
6412
|
+
const baseline = round17(y + font * 0.8);
|
|
6413
|
+
let out = `<text x="${x}" y="${baseline}" font-family="${FONT_FAMILY}" font-size="${font}" fill="${LABEL_FILL7}">${xmlEscape(text)}</text>`;
|
|
6414
|
+
if (f.isStatic) {
|
|
6415
|
+
const lineY = round17(baseline + 1);
|
|
6416
|
+
const approxW = round17(text.length * font * 0.6);
|
|
6417
|
+
out += `<line x1="${x}" y1="${lineY}" x2="${round17(x + approxW)}" y2="${lineY}" stroke="${LABEL_FILL7}" stroke-width="0.8"/>`;
|
|
6418
|
+
}
|
|
6419
|
+
return out;
|
|
6420
|
+
}
|
|
6421
|
+
function boxSvg2(node, cls) {
|
|
6422
|
+
const left = round17(node.cx - node.boxW / 2);
|
|
6423
|
+
const top = round17(node.cy - node.boxH / 2);
|
|
6424
|
+
const metrics = node.metrics;
|
|
6425
|
+
const pieces = [];
|
|
6426
|
+
pieces.push(`<title>${xmlEscape(node.title)}</title>`);
|
|
6427
|
+
pieces.push(
|
|
6428
|
+
`<rect x="${left}" y="${top}" width="${node.boxW}" height="${node.boxH}" rx="2" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`
|
|
6429
|
+
);
|
|
6430
|
+
for (const dy of metrics.dividerYs) {
|
|
6431
|
+
const y = round17(top + dy);
|
|
6432
|
+
pieces.push(
|
|
6433
|
+
`<line x1="${left}" y1="${y}" x2="${round17(left + node.boxW)}" y2="${y}" stroke="${GLYPH_STROKE6}" stroke-width="0.75"/>`
|
|
6434
|
+
);
|
|
6435
|
+
}
|
|
6436
|
+
const compartment0 = metrics.rows[0];
|
|
6437
|
+
const textX = round17(left + node.boxW / 2);
|
|
6438
|
+
let lineIdx = 0;
|
|
6439
|
+
if (cls.stereotype !== null) {
|
|
6440
|
+
const stereoY = round17(top + compartment0.top + lineIdx * UML_LINE_H + UML_NAME_FONT * 0.8);
|
|
6441
|
+
pieces.push(
|
|
6442
|
+
`<text x="${textX}" y="${stereoY}" font-family="${FONT_FAMILY}" font-size="${UML_NAME_FONT}" fill="${LABEL_FILL7}" text-anchor="middle">${xmlEscape(cls.stereotype)}</text>`
|
|
6443
|
+
);
|
|
6444
|
+
lineIdx++;
|
|
6445
|
+
}
|
|
6446
|
+
const nameY = round17(top + compartment0.top + lineIdx * UML_LINE_H + UML_NAME_FONT * 0.8);
|
|
6447
|
+
const nameStyle = cls.isAbstract ? ` font-style="italic"` : "";
|
|
6448
|
+
const nameText = metrics.rows[0].lines[lineIdx] ?? "";
|
|
6449
|
+
pieces.push(
|
|
6450
|
+
`<text x="${textX}" y="${nameY}" font-family="${FONT_FAMILY}" font-size="${UML_NAME_FONT}" fill="${LABEL_FILL7}" text-anchor="middle"${nameStyle}>${xmlEscape(nameText)}</text>`
|
|
6451
|
+
);
|
|
6452
|
+
const attrCompartment = metrics.rows[1];
|
|
6453
|
+
const attrX = round17(left + UML_BOX_PAD_X);
|
|
6454
|
+
for (let i = 0; i < cls.attributes.length; i++) {
|
|
6455
|
+
const f = cls.attributes[i];
|
|
6456
|
+
const fy = round17(top + attrCompartment.top + i * UML_LINE_H);
|
|
6457
|
+
pieces.push(featureSvg(f, attrX, fy, UML_FEAT_FONT));
|
|
6458
|
+
}
|
|
6459
|
+
const opCompartment = metrics.rows[2];
|
|
6460
|
+
const opX = round17(left + UML_BOX_PAD_X);
|
|
6461
|
+
for (let i = 0; i < cls.operations.length; i++) {
|
|
6462
|
+
const f = cls.operations[i];
|
|
6463
|
+
const fy = round17(top + opCompartment.top + i * UML_LINE_H);
|
|
6464
|
+
pieces.push(featureSvg(f, opX, fy, UML_FEAT_FONT));
|
|
6465
|
+
}
|
|
6466
|
+
return `<g data-node-id="c${node.classId}">${pieces.join("")}</g>`;
|
|
6467
|
+
}
|
|
6468
|
+
function edgeSvg(el, rel, textBoxes) {
|
|
6469
|
+
const glyphs = RELATION_GLYPHS[rel.kind];
|
|
6470
|
+
const isDashed = glyphs.line === "dashed";
|
|
6471
|
+
const stroke = isDashed ? EDGE_STROKE.distant : { width: 1.5, opacity: 0.7 };
|
|
6472
|
+
const strokeAttrs = isDashed ? `stroke="${EDGE_INK8}" stroke-width="${stroke.width}" stroke-dasharray="4,4" opacity="${stroke.opacity}"` : `stroke="${EDGE_INK8}" stroke-width="${stroke.width}" opacity="${stroke.opacity}"`;
|
|
6473
|
+
const pts = el.points;
|
|
6474
|
+
const pieces = [`<title>${xmlEscape(el.title)}</title>`];
|
|
6475
|
+
if (pts.length >= 2) {
|
|
6476
|
+
pieces.push(`<polyline points="${pts.map((p) => `${p.x},${p.y}`).join(" ")}" fill="none" ${strokeAttrs}/>`);
|
|
6477
|
+
if (glyphs.sourceEnd !== "none") {
|
|
6478
|
+
const tip = pts[0];
|
|
6479
|
+
const next = pts[1];
|
|
6480
|
+
const dir = glyphDir(tip, next);
|
|
6481
|
+
pieces.push(emitGlyph(glyphs.sourceEnd, tip, dir));
|
|
6482
|
+
}
|
|
6483
|
+
if (glyphs.targetEnd !== "none") {
|
|
6484
|
+
const tip = pts[pts.length - 1];
|
|
6485
|
+
const prev = pts[pts.length - 2];
|
|
6486
|
+
const dir = glyphDir(tip, prev);
|
|
6487
|
+
pieces.push(emitGlyph(glyphs.targetEnd, tip, dir));
|
|
6488
|
+
}
|
|
6489
|
+
for (const tb of textBoxes) {
|
|
6490
|
+
if (tb.relId !== rel.id) continue;
|
|
6491
|
+
const italic = tb.slot === "src-role" || tb.slot === "tgt-role" ? ` font-style="italic"` : "";
|
|
6492
|
+
const anchor = tb.anchor === "middle" ? ` text-anchor="middle"` : "";
|
|
6493
|
+
const drawX = tb.anchor === "middle" ? round17(tb.x + tb.w / 2) : tb.x;
|
|
6494
|
+
const baseline = round17(tb.y + UML_LABEL_FONT * 0.8);
|
|
6495
|
+
pieces.push(
|
|
6496
|
+
`<text x="${drawX}" y="${baseline}" font-family="${FONT_FAMILY}" font-size="${UML_LABEL_FONT}" fill="${LABEL_FILL7}"${anchor}${italic}>${xmlEscape(tb.text)}</text>`
|
|
6497
|
+
);
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
return `<g data-edge-id="${el.edgeId}">${pieces.join("")}</g>`;
|
|
6501
|
+
}
|
|
6502
|
+
function makeSwatchLine(isDashed) {
|
|
6503
|
+
return (x, y) => {
|
|
6504
|
+
const attrs = isDashed ? `stroke="${EDGE_INK8}" stroke-width="1.5" stroke-dasharray="4,4"` : `stroke="${EDGE_INK8}" stroke-width="1.5"`;
|
|
6505
|
+
return `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" ${attrs}/>`;
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
function umlLayoutSvg(layout, input, opts = {}) {
|
|
6509
|
+
const labels = opts.labels ?? UML_SVG_LABELS_EN;
|
|
6510
|
+
const showLegend = opts.legend !== false;
|
|
6511
|
+
const classMap = new Map(input.classes.map((c) => [c.id, c]));
|
|
6512
|
+
const relMap = new Map(input.relationships.map((r2) => [r2.id, r2]));
|
|
6513
|
+
const parts = [];
|
|
6514
|
+
for (const node of layout.nodes) {
|
|
6515
|
+
const cls = classMap.get(node.classId);
|
|
6516
|
+
parts.push(boxSvg2(node, cls));
|
|
6517
|
+
}
|
|
6518
|
+
for (const el of layout.elements) {
|
|
6519
|
+
const rel = relMap.get(el.relId);
|
|
6520
|
+
if (!rel) continue;
|
|
6521
|
+
parts.push(edgeSvg(el, rel, layout.textBoxes));
|
|
6522
|
+
}
|
|
6523
|
+
let canvasW = layout.canvasW;
|
|
6524
|
+
let canvasH = layout.canvasH;
|
|
6525
|
+
if (showLegend) {
|
|
6526
|
+
const usedKinds = new Set(
|
|
6527
|
+
input.relationships.map((r2) => r2.kind)
|
|
6528
|
+
);
|
|
6529
|
+
const entries = [];
|
|
6530
|
+
for (const kind of UML_RELATIONSHIPS) {
|
|
6531
|
+
if (!usedKinds.has(kind)) continue;
|
|
6532
|
+
const isDashed = RELATION_GLYPHS[kind].line === "dashed";
|
|
6533
|
+
entries.push({
|
|
6534
|
+
swatch: makeSwatchLine(isDashed),
|
|
6535
|
+
label: labels.legend[kind]
|
|
6536
|
+
});
|
|
6537
|
+
}
|
|
6538
|
+
if (entries.length > 0) {
|
|
6539
|
+
const block = legendBlock(entries, canvasH);
|
|
6540
|
+
parts.push(block.svg);
|
|
6541
|
+
canvasH = block.height;
|
|
6542
|
+
canvasW = Math.max(canvasW, block.width);
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
const roundedW = round17(canvasW);
|
|
6546
|
+
const roundedH = round17(canvasH);
|
|
6547
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${roundedW}" height="${roundedH}" viewBox="0 0 ${roundedW} ${roundedH}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
6548
|
+
}
|
|
6549
|
+
|
|
6550
|
+
// src/uml/render.ts
|
|
6551
|
+
function umlSvg(input, opts = {}) {
|
|
6552
|
+
const layout = computeUmlLayout(input, opts.titleLabels ?? UML_TITLE_LABELS_EN);
|
|
6553
|
+
const svg = umlLayoutSvg(layout, input, opts);
|
|
6554
|
+
return { svg, layout };
|
|
6555
|
+
}
|
|
6556
|
+
|
|
4744
6557
|
exports.ANNOTATION_INK = ANNOTATION_INK;
|
|
4745
6558
|
exports.CHAR_W = CHAR_W;
|
|
4746
6559
|
exports.CODE_FONT = CODE_FONT;
|
|
@@ -4764,6 +6577,12 @@ exports.FishboneValidationError = FishboneValidationError;
|
|
|
4764
6577
|
exports.GATE_TYPES = GATE_TYPES;
|
|
4765
6578
|
exports.GENOGRAM_SVG_LABELS_EN = GENOGRAM_SVG_LABELS_EN;
|
|
4766
6579
|
exports.GENOGRAM_TITLE_LABELS_EN = GENOGRAM_TITLE_LABELS_EN;
|
|
6580
|
+
exports.GLYPH_ARROW_HALF = GLYPH_ARROW_HALF;
|
|
6581
|
+
exports.GLYPH_ARROW_LEN = GLYPH_ARROW_LEN;
|
|
6582
|
+
exports.GLYPH_DIAMOND_HALF = GLYPH_DIAMOND_HALF;
|
|
6583
|
+
exports.GLYPH_DIAMOND_LEN = GLYPH_DIAMOND_LEN;
|
|
6584
|
+
exports.GLYPH_TRI_HALF = GLYPH_TRI_HALF;
|
|
6585
|
+
exports.GLYPH_TRI_LEN = GLYPH_TRI_LEN;
|
|
4767
6586
|
exports.KINSHIP_EN = KINSHIP_EN;
|
|
4768
6587
|
exports.LABEL_FONT = LABEL_FONT;
|
|
4769
6588
|
exports.LABEL_GAP = LABEL_GAP;
|
|
@@ -4815,16 +6634,63 @@ exports.PHYLO_SCALEBAR_ID = PHYLO_SCALEBAR_ID;
|
|
|
4815
6634
|
exports.PHYLO_SUPPORT_FONT = PHYLO_SUPPORT_FONT;
|
|
4816
6635
|
exports.PHYLO_SVG_LABELS_EN = PHYLO_SVG_LABELS_EN;
|
|
4817
6636
|
exports.PHYLO_TITLE_LABELS_EN = PHYLO_TITLE_LABELS_EN;
|
|
6637
|
+
exports.PRISMA_BAND_LABEL_W = PRISMA_BAND_LABEL_W;
|
|
6638
|
+
exports.PRISMA_BOX_KINDS = PRISMA_BOX_KINDS;
|
|
6639
|
+
exports.PRISMA_BOX_PAD_X = PRISMA_BOX_PAD_X;
|
|
6640
|
+
exports.PRISMA_BOX_PAD_Y = PRISMA_BOX_PAD_Y;
|
|
6641
|
+
exports.PRISMA_COLUMNS = PRISMA_COLUMNS;
|
|
6642
|
+
exports.PRISMA_COL_GAP = PRISMA_COL_GAP;
|
|
6643
|
+
exports.PRISMA_CORRIDOR = PRISMA_CORRIDOR;
|
|
6644
|
+
exports.PRISMA_COUNT_FONT = PRISMA_COUNT_FONT;
|
|
6645
|
+
exports.PRISMA_EXCL_ARROW_BASE = PRISMA_EXCL_ARROW_BASE;
|
|
6646
|
+
exports.PRISMA_EXCL_GAP = PRISMA_EXCL_GAP;
|
|
6647
|
+
exports.PRISMA_FLOW_ARROW_BASE = PRISMA_FLOW_ARROW_BASE;
|
|
6648
|
+
exports.PRISMA_HEADING_FONT = PRISMA_HEADING_FONT;
|
|
6649
|
+
exports.PRISMA_LINE_H = PRISMA_LINE_H;
|
|
6650
|
+
exports.PRISMA_MERGE_ARROW_BASE = PRISMA_MERGE_ARROW_BASE;
|
|
6651
|
+
exports.PRISMA_MIN_BOX_W = PRISMA_MIN_BOX_W;
|
|
6652
|
+
exports.PRISMA_PADDING = PRISMA_PADDING;
|
|
6653
|
+
exports.PRISMA_PHASES = PRISMA_PHASES;
|
|
6654
|
+
exports.PRISMA_SVG_LABELS_EN = PRISMA_SVG_LABELS_EN;
|
|
6655
|
+
exports.PRISMA_TITLE_LABELS_EN = PRISMA_TITLE_LABELS_EN;
|
|
4818
6656
|
exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
|
|
4819
6657
|
exports.PedigreeValidationError = PedigreeValidationError;
|
|
4820
6658
|
exports.PhyloValidationError = PhyloValidationError;
|
|
6659
|
+
exports.PrismaValidationError = PrismaValidationError;
|
|
4821
6660
|
exports.QUALITY_LEXICON_EN = QUALITY_LEXICON_EN;
|
|
6661
|
+
exports.RELATION_GLYPHS = RELATION_GLYPHS;
|
|
6662
|
+
exports.UML_ASSOC_ROUTE_BASE = UML_ASSOC_ROUTE_BASE;
|
|
6663
|
+
exports.UML_BOX_PAD_X = UML_BOX_PAD_X;
|
|
6664
|
+
exports.UML_BOX_PAD_Y = UML_BOX_PAD_Y;
|
|
6665
|
+
exports.UML_COL_GAP = UML_COL_GAP;
|
|
6666
|
+
exports.UML_FEAT_FONT = UML_FEAT_FONT;
|
|
6667
|
+
exports.UML_GLYPH_BASE = UML_GLYPH_BASE;
|
|
6668
|
+
exports.UML_LABEL_FONT = UML_LABEL_FONT;
|
|
6669
|
+
exports.UML_LINE_H = UML_LINE_H;
|
|
6670
|
+
exports.UML_MIN_BOX_W = UML_MIN_BOX_W;
|
|
6671
|
+
exports.UML_NAME_FONT = UML_NAME_FONT;
|
|
6672
|
+
exports.UML_PADDING = UML_PADDING;
|
|
6673
|
+
exports.UML_PORT_PITCH = UML_PORT_PITCH;
|
|
6674
|
+
exports.UML_RELATIONSHIPS = UML_RELATIONSHIPS;
|
|
6675
|
+
exports.UML_ROW_GAP = UML_ROW_GAP;
|
|
6676
|
+
exports.UML_SELF_LOOP_BASE = UML_SELF_LOOP_BASE;
|
|
6677
|
+
exports.UML_SELF_LOOP_W = UML_SELF_LOOP_W;
|
|
6678
|
+
exports.UML_SEP_PAD = UML_SEP_PAD;
|
|
6679
|
+
exports.UML_SVG_LABELS_EN = UML_SVG_LABELS_EN;
|
|
6680
|
+
exports.UML_TITLE_LABELS_EN = UML_TITLE_LABELS_EN;
|
|
6681
|
+
exports.UML_VISIBILITIES = UML_VISIBILITIES;
|
|
6682
|
+
exports.UML_VIS_GLYPH = UML_VIS_GLYPH;
|
|
4822
6683
|
exports.UNION_NOTATION = UNION_NOTATION;
|
|
4823
6684
|
exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
|
|
4824
6685
|
exports.UNION_STATUSES = UNION_STATUSES;
|
|
6686
|
+
exports.UmlValidationError = UmlValidationError;
|
|
6687
|
+
exports.allocateLanes = allocateLanes;
|
|
4825
6688
|
exports.annotationDot = annotationDot;
|
|
4826
6689
|
exports.annotationSwatch = annotationSwatch;
|
|
4827
6690
|
exports.annotationTick = annotationTick;
|
|
6691
|
+
exports.arrowFilledPoints = arrowFilledPoints;
|
|
6692
|
+
exports.arrowOpenPoints = arrowOpenPoints;
|
|
6693
|
+
exports.bandStack = bandStack;
|
|
4828
6694
|
exports.clampLabel = clampLabel;
|
|
4829
6695
|
exports.classifyRelationshipType = classifyRelationshipType;
|
|
4830
6696
|
exports.computeFaultTreeLayout = computeFaultTreeLayout;
|
|
@@ -4832,6 +6698,9 @@ exports.computeGenogramLayout = computeGenogramLayout;
|
|
|
4832
6698
|
exports.computeOrgChartLayout = computeOrgChartLayout;
|
|
4833
6699
|
exports.computePedigreeLayout = computePedigreeLayout;
|
|
4834
6700
|
exports.computePhyloLayout = computePhyloLayout;
|
|
6701
|
+
exports.computePrismaLayout = computePrismaLayout;
|
|
6702
|
+
exports.computeUmlLayout = computeUmlLayout;
|
|
6703
|
+
exports.diamondPoints = diamondPoints;
|
|
4835
6704
|
exports.ecomapSvg = ecomapSvg;
|
|
4836
6705
|
exports.estimateTextWidth = estimateTextWidth;
|
|
4837
6706
|
exports.faultTreeIssues = faultTreeIssues;
|
|
@@ -4840,13 +6709,16 @@ exports.faultTreeSvg = faultTreeSvg;
|
|
|
4840
6709
|
exports.fishboneSvg = fishboneSvg;
|
|
4841
6710
|
exports.genogramLayoutSvg = genogramLayoutSvg;
|
|
4842
6711
|
exports.genogramSvg = genogramSvg;
|
|
6712
|
+
exports.glyphInset = glyphInset;
|
|
4843
6713
|
exports.latestUnionPerPair = latestUnionPerPair;
|
|
4844
6714
|
exports.legendBlock = legendBlock;
|
|
6715
|
+
exports.measureCompartmentBox = measureCompartmentBox;
|
|
4845
6716
|
exports.niceScaleStep = niceScaleStep;
|
|
4846
6717
|
exports.normalizeText = normalizeText;
|
|
4847
6718
|
exports.orgChartIssues = orgChartIssues;
|
|
4848
6719
|
exports.orgChartLayoutSvg = orgChartLayoutSvg;
|
|
4849
6720
|
exports.orgChartSvg = orgChartSvg;
|
|
6721
|
+
exports.packGrid = packGrid;
|
|
4850
6722
|
exports.packSubtree = packSubtree;
|
|
4851
6723
|
exports.pathData = pathData;
|
|
4852
6724
|
exports.pedigreeIssues = pedigreeIssues;
|
|
@@ -4855,13 +6727,24 @@ exports.pedigreeSvg = pedigreeSvg;
|
|
|
4855
6727
|
exports.phyloIssues = phyloIssues;
|
|
4856
6728
|
exports.phyloLayoutSvg = phyloLayoutSvg;
|
|
4857
6729
|
exports.phyloSvg = phyloSvg;
|
|
6730
|
+
exports.prismaIssues = prismaIssues;
|
|
6731
|
+
exports.prismaLayoutSvg = prismaLayoutSvg;
|
|
6732
|
+
exports.prismaSvg = prismaSvg;
|
|
4858
6733
|
exports.qualityLineStyle = qualityLineStyle;
|
|
4859
6734
|
exports.relationshipTypeTokens = relationshipTypeTokens;
|
|
4860
6735
|
exports.romanNumeral = romanNumeral;
|
|
6736
|
+
exports.sideCapacity = sideCapacity;
|
|
6737
|
+
exports.sideCapacityIssue = sideCapacityIssue;
|
|
6738
|
+
exports.trianglePoints = trianglePoints;
|
|
6739
|
+
exports.umlIssues = umlIssues;
|
|
6740
|
+
exports.umlLayoutSvg = umlLayoutSvg;
|
|
6741
|
+
exports.umlSvg = umlSvg;
|
|
4861
6742
|
exports.validateFaultTree = validateFaultTree;
|
|
4862
6743
|
exports.validateOrgChart = validateOrgChart;
|
|
4863
6744
|
exports.validatePedigree = validatePedigree;
|
|
4864
6745
|
exports.validatePhylo = validatePhylo;
|
|
6746
|
+
exports.validatePrisma = validatePrisma;
|
|
6747
|
+
exports.validateUml = validateUml;
|
|
4865
6748
|
exports.wrapLabel = wrapLabel;
|
|
4866
6749
|
exports.wrapLabelBalanced = wrapLabelBalanced;
|
|
4867
6750
|
exports.xmlEscape = xmlEscape;
|