compasso 0.4.0 → 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 +115 -5
- package/dist/{chunk-ZBDABVIO.js → chunk-2NDET6O5.js} +3 -3
- package/dist/{chunk-ZBDABVIO.js.map → chunk-2NDET6O5.js.map} +1 -1
- package/dist/{chunk-Q6DVTCXD.js → chunk-3RGYLVTN.js} +18 -6
- package/dist/chunk-3RGYLVTN.js.map +1 -0
- 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-F47C6ZEB.js → chunk-JBDA7E2O.js} +3 -3
- package/dist/{chunk-F47C6ZEB.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-JP4N42AY.js → chunk-PJHLWSGD.js} +3 -3
- package/dist/{chunk-JP4N42AY.js.map → chunk-PJHLWSGD.js.map} +1 -1
- package/dist/{chunk-LRHHUJFZ.js → chunk-RDH4XHA2.js} +3 -3
- package/dist/{chunk-LRHHUJFZ.js.map → chunk-RDH4XHA2.js.map} +1 -1
- package/dist/{chunk-UJVU7B44.js → chunk-WEHUSHVI.js} +31 -51
- package/dist/chunk-WEHUSHVI.js.map +1 -0
- package/dist/{chunk-RWPGGWO5.js → chunk-Z66YUOUM.js} +34 -10
- package/dist/chunk-Z66YUOUM.js.map +1 -0
- package/dist/core/index.cjs +247 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +94 -2
- package/dist/core/index.d.ts +94 -2
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +34 -11
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.d.cts +12 -0
- package/dist/ecomap/index.d.ts +12 -0
- 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.cjs +57 -7
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +22 -6
- package/dist/genogram/index.d.ts +22 -6
- 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 +2360 -395
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -7
- package/dist/index.d.ts +14 -7
- package/dist/index.js +10 -8
- package/dist/{kinship-DqEklrDN.d.ts → kinship-BF90HyyS.d.ts} +1 -1
- package/dist/{kinship-Dy_ijjJV.d.cts → kinship-BOUss5cT.d.cts} +1 -1
- package/dist/{labels-RtFw9tX1.d.cts → labels-B0aOMbHy.d.cts} +12 -0
- package/dist/{labels-RtFw9tX1.d.ts → labels-B0aOMbHy.d.ts} +12 -0
- package/dist/labels-Br8yjc3C.d.cts +29 -0
- package/dist/labels-Br8yjc3C.d.ts +29 -0
- package/dist/{labels-DNqRkWuI.d.ts → labels-CuLbFyrz.d.ts} +1 -1
- package/dist/labels-D1v1RWZd.d.cts +97 -0
- package/dist/labels-D1v1RWZd.d.ts +97 -0
- package/dist/{labels-CBQ_3Ec9.d.cts → labels-DhQe7I8m.d.cts} +1 -1
- 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 +11 -5
- package/dist/locales/pt-br.d.ts +11 -5
- package/dist/locales/pt-br.js +50 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/org-chart/index.cjs +138 -94
- package/dist/org-chart/index.cjs.map +1 -1
- package/dist/org-chart/index.d.cts +24 -33
- package/dist/org-chart/index.d.ts +24 -33
- package/dist/org-chart/index.js +2 -2
- package/dist/pedigree/index.d.cts +6 -6
- package/dist/pedigree/index.d.ts +6 -6
- 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/{types-BnMG7TCd.d.cts → types-jE2fdM1t.d.cts} +8 -0
- package/dist/{types-BnMG7TCd.d.ts → types-jE2fdM1t.d.ts} +8 -0
- 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-O3BT2O42.js +0 -145
- package/dist/chunk-O3BT2O42.js.map +0 -1
- package/dist/chunk-Q6DVTCXD.js.map +0 -1
- package/dist/chunk-RWPGGWO5.js.map +0 -1
- package/dist/chunk-UJVU7B44.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -142,6 +142,233 @@ function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
|
|
|
142
142
|
return matched.length === 1 ? matched[0] : "plain";
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// src/core/annotation.ts
|
|
146
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
147
|
+
var ANNOTATION_INK = "#52525b";
|
|
148
|
+
var DOT_R = 3;
|
|
149
|
+
var TICK_HALF = 3;
|
|
150
|
+
function annotationDot(cx, cy) {
|
|
151
|
+
return `<circle cx="${round(cx)}" cy="${round(cy)}" r="${DOT_R}" fill="${ANNOTATION_INK}" fill-opacity="0.6"/>`;
|
|
152
|
+
}
|
|
153
|
+
function annotationTick(points) {
|
|
154
|
+
if (points.length < 2) return "";
|
|
155
|
+
const i = Math.floor((points.length - 1) / 2);
|
|
156
|
+
const a = points[i];
|
|
157
|
+
const b = points[i + 1] ?? points[i];
|
|
158
|
+
const mx = (a.x + b.x) / 2;
|
|
159
|
+
const my = (a.y + b.y) / 2;
|
|
160
|
+
const horizontal = Math.abs(a.y - b.y) <= Math.abs(a.x - b.x);
|
|
161
|
+
const x1 = horizontal ? mx : mx - TICK_HALF;
|
|
162
|
+
const x2 = horizontal ? mx : mx + TICK_HALF;
|
|
163
|
+
const y1 = horizontal ? my - TICK_HALF : my;
|
|
164
|
+
const y2 = horizontal ? my + TICK_HALF : my;
|
|
165
|
+
return `<line x1="${round(x1)}" y1="${round(y1)}" x2="${round(x2)}" y2="${round(y2)}" stroke="${ANNOTATION_INK}" stroke-width="1.5"/>`;
|
|
166
|
+
}
|
|
167
|
+
function annotationSwatch(x, yCenter) {
|
|
168
|
+
return annotationDot(x + LEGEND_SWATCH_W / 2, yCenter);
|
|
169
|
+
}
|
|
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
|
+
|
|
145
372
|
// src/genogram/types.ts
|
|
146
373
|
var UNION_STATUSES = [
|
|
147
374
|
"married",
|
|
@@ -276,7 +503,7 @@ function arrivalOffset(slot, total) {
|
|
|
276
503
|
const sign = slot % 2 === 1 ? -1 : 1;
|
|
277
504
|
return sign * Math.ceil(slot / 2) * step;
|
|
278
505
|
}
|
|
279
|
-
function
|
|
506
|
+
function allocateLanes2(items) {
|
|
280
507
|
const lanes = [];
|
|
281
508
|
for (const it of items) {
|
|
282
509
|
let chosen = -1;
|
|
@@ -307,36 +534,37 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
307
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);
|
|
308
535
|
const coupleUnions = validUnions.filter((u) => u.status !== "coparental");
|
|
309
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);
|
|
310
|
-
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);
|
|
311
538
|
const relClass = new Map(
|
|
312
|
-
validRels.map((
|
|
539
|
+
validRels.map((r2) => [r2.id, classifyRelationshipType(r2.type, kinship)])
|
|
313
540
|
);
|
|
314
|
-
const bondRels = validRels.filter((
|
|
315
|
-
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");
|
|
316
543
|
const realLinks = validLinks.map((l) => ({
|
|
317
544
|
parentId: l.parentId,
|
|
318
545
|
childId: l.childId,
|
|
319
546
|
quality: l.quality,
|
|
320
|
-
edgeId: PARENT_REL_ID_BASE + l.id
|
|
547
|
+
edgeId: PARENT_REL_ID_BASE + l.id,
|
|
548
|
+
annotated: l.annotated ?? false
|
|
321
549
|
}));
|
|
322
550
|
const declaredPairs = new Set(validLinks.map((l) => pairKey(l.parentId, l.childId)));
|
|
323
551
|
const promotedByPair = /* @__PURE__ */ new Map();
|
|
324
|
-
for (const
|
|
325
|
-
const key = pairKey(
|
|
552
|
+
for (const r2 of parentageRels) {
|
|
553
|
+
const key = pairKey(r2.fromPersonId, r2.toPersonId);
|
|
326
554
|
if (declaredPairs.has(key)) continue;
|
|
327
555
|
if (promotedByPair.has(key)) continue;
|
|
328
|
-
const ga = genById.get(
|
|
329
|
-
const gb = genById.get(
|
|
556
|
+
const ga = genById.get(r2.fromPersonId) ?? null;
|
|
557
|
+
const gb = genById.get(r2.toPersonId) ?? null;
|
|
330
558
|
let parentId;
|
|
331
559
|
let childId;
|
|
332
560
|
if (ga !== null && gb !== null && ga !== gb) {
|
|
333
|
-
[parentId, childId] = ga < gb ? [
|
|
561
|
+
[parentId, childId] = ga < gb ? [r2.fromPersonId, r2.toPersonId] : [r2.toPersonId, r2.fromPersonId];
|
|
334
562
|
} else {
|
|
335
|
-
const fromIsChild = relationshipTypeTokens(
|
|
336
|
-
[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];
|
|
337
565
|
}
|
|
338
566
|
if (parentId === childId) continue;
|
|
339
|
-
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 });
|
|
340
568
|
}
|
|
341
569
|
const allLinks = [...realLinks, ...promotedByPair.values()].sort((a, b) => a.edgeId - b.edgeId);
|
|
342
570
|
const coupleByPair = /* @__PURE__ */ new Map();
|
|
@@ -358,9 +586,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
358
586
|
hasTie.add(l.parentId);
|
|
359
587
|
hasTie.add(l.childId);
|
|
360
588
|
}
|
|
361
|
-
for (const
|
|
362
|
-
hasTie.add(
|
|
363
|
-
hasTie.add(
|
|
589
|
+
for (const r2 of bondRels) {
|
|
590
|
+
hasTie.add(r2.fromPersonId);
|
|
591
|
+
hasTie.add(r2.toPersonId);
|
|
364
592
|
}
|
|
365
593
|
const isIsolated = (id) => !hasTie.has(id);
|
|
366
594
|
const byGen = /* @__PURE__ */ new Map();
|
|
@@ -376,13 +604,13 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
376
604
|
});
|
|
377
605
|
const rowCount = genKeys.length;
|
|
378
606
|
const rowOfPerson = /* @__PURE__ */ new Map();
|
|
379
|
-
genKeys.forEach((g,
|
|
380
|
-
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) => {
|
|
381
609
|
const members = byGen.get(g).map((p) => p.id);
|
|
382
610
|
const adj = /* @__PURE__ */ new Map();
|
|
383
611
|
for (const id of members) adj.set(id, []);
|
|
384
612
|
for (const u of coupleUnions) {
|
|
385
|
-
if (rowOfPerson.get(u.personAId) ===
|
|
613
|
+
if (rowOfPerson.get(u.personAId) === r2 && rowOfPerson.get(u.personBId) === r2) {
|
|
386
614
|
adj.get(u.personAId).push(u.personBId);
|
|
387
615
|
adj.get(u.personBId).push(u.personAId);
|
|
388
616
|
}
|
|
@@ -438,9 +666,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
438
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);
|
|
439
667
|
return w;
|
|
440
668
|
};
|
|
441
|
-
const sweep = (
|
|
669
|
+
const sweep = (r2, ref) => {
|
|
442
670
|
const pos = flattened();
|
|
443
|
-
const blocks = rowBlocks[
|
|
671
|
+
const blocks = rowBlocks[r2];
|
|
444
672
|
const bcOf = (b) => {
|
|
445
673
|
let sum = 0;
|
|
446
674
|
let wsum = 0;
|
|
@@ -456,23 +684,23 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
456
684
|
};
|
|
457
685
|
const keyed = blocks.map((b) => ({ b, bc: bcOf(b), minId: Math.min(...b) }));
|
|
458
686
|
keyed.sort((x, y) => x.bc !== y.bc ? x.bc - y.bc : x.minId - y.minId);
|
|
459
|
-
rowBlocks[
|
|
687
|
+
rowBlocks[r2] = keyed.map((k) => k.b);
|
|
460
688
|
};
|
|
461
689
|
if (rowCount >= 2) {
|
|
462
|
-
for (let
|
|
463
|
-
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);
|
|
464
692
|
}
|
|
465
|
-
for (let
|
|
466
|
-
const blocks = rowBlocks[
|
|
693
|
+
for (let r2 = 0; r2 < rowCount; r2++) {
|
|
694
|
+
const blocks = rowBlocks[r2];
|
|
467
695
|
const connected = blocks.filter((b) => !(b.length === 1 && isIsolated(b[0])));
|
|
468
696
|
const isolated = blocks.filter((b) => b.length === 1 && isIsolated(b[0])).sort((a, b) => a[0] - b[0]);
|
|
469
|
-
rowBlocks[
|
|
697
|
+
rowBlocks[r2] = [...connected, ...isolated];
|
|
470
698
|
}
|
|
471
699
|
const colOf = /* @__PURE__ */ new Map();
|
|
472
700
|
let colCount = 0;
|
|
473
|
-
for (let
|
|
701
|
+
for (let r2 = 0; r2 < rowCount; r2++) {
|
|
474
702
|
let c = 0;
|
|
475
|
-
for (const b of rowBlocks[
|
|
703
|
+
for (const b of rowBlocks[r2]) for (const id of b) colOf.set(id, c++);
|
|
476
704
|
colCount = Math.max(colCount, c);
|
|
477
705
|
}
|
|
478
706
|
const colOrThrow = (id) => colOf.get(id);
|
|
@@ -499,8 +727,8 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
499
727
|
}
|
|
500
728
|
const rowMaxLines = Array.from({ length: rowCount }, () => 1);
|
|
501
729
|
for (const p of people) {
|
|
502
|
-
const
|
|
503
|
-
rowMaxLines[
|
|
730
|
+
const r2 = rowOfPerson.get(p.id);
|
|
731
|
+
rowMaxLines[r2] = Math.max(rowMaxLines[r2], measured.get(p.id).lines.length);
|
|
504
732
|
}
|
|
505
733
|
const planned = [];
|
|
506
734
|
const gutterReqs = [];
|
|
@@ -619,10 +847,10 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
619
847
|
{
|
|
620
848
|
const byRow = /* @__PURE__ */ new Map();
|
|
621
849
|
for (const u of barUnions) {
|
|
622
|
-
const
|
|
623
|
-
(byRow.get(
|
|
850
|
+
const r2 = rowOfPerson.get(u.personAId);
|
|
851
|
+
(byRow.get(r2) ?? byRow.set(r2, []).get(r2)).push(u);
|
|
624
852
|
}
|
|
625
|
-
for (const [
|
|
853
|
+
for (const [r2, rowUnions] of byRow) {
|
|
626
854
|
const unionsOfPerson = /* @__PURE__ */ new Map();
|
|
627
855
|
for (const u of rowUnions) {
|
|
628
856
|
(unionsOfPerson.get(u.personAId) ?? unionsOfPerson.set(u.personAId, []).get(u.personAId)).push(u);
|
|
@@ -648,7 +876,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
648
876
|
}
|
|
649
877
|
comp.sort((a, b) => a.id - b.id);
|
|
650
878
|
comp.forEach((u, i) => unionDipLevel.set(u.id, i));
|
|
651
|
-
rowDipLevels[
|
|
879
|
+
rowDipLevels[r2] = Math.max(rowDipLevels[r2], comp.length - 1);
|
|
652
880
|
}
|
|
653
881
|
}
|
|
654
882
|
}
|
|
@@ -669,6 +897,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
669
897
|
toPersonId: u.personBId,
|
|
670
898
|
titles: [unionTitle(u)],
|
|
671
899
|
lineStyle: "plain",
|
|
900
|
+
annotated: u.annotated ?? false,
|
|
672
901
|
build: () => {
|
|
673
902
|
const cy = geo.cy(rowOfPerson.get(leftId));
|
|
674
903
|
if (dipLevel === 0) {
|
|
@@ -703,6 +932,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
703
932
|
toPersonId: u.personBId,
|
|
704
933
|
titles: [unionTitle(u)],
|
|
705
934
|
lineStyle: "plain",
|
|
935
|
+
annotated: u.annotated ?? false,
|
|
706
936
|
build
|
|
707
937
|
});
|
|
708
938
|
}
|
|
@@ -759,6 +989,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
759
989
|
};
|
|
760
990
|
gutterReqs.push(spineReq);
|
|
761
991
|
const aId = (c) => linkOf.get(`${u.personAId}>${c}`).edgeId;
|
|
992
|
+
const aLinkAnnotated = (c) => linkOf.get(`${u.personAId}>${c}`).annotated ?? false;
|
|
762
993
|
const childDrop = (c, parentId) => {
|
|
763
994
|
const link = linkOf.get(`${parentId}>${c}`);
|
|
764
995
|
const edgeId = link.edgeId;
|
|
@@ -771,6 +1002,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
771
1002
|
toPersonId: c,
|
|
772
1003
|
titles: [linkTitle(link)],
|
|
773
1004
|
lineStyle: "plain",
|
|
1005
|
+
annotated: link.annotated,
|
|
774
1006
|
build: () => {
|
|
775
1007
|
const y = geo.corridorLaneY(sibReq);
|
|
776
1008
|
const x = geo.cx(colOrThrow(c)) + arrivalOffset(arr.slot, arrivalCount.get(c) ?? 1);
|
|
@@ -791,6 +1023,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
791
1023
|
toPersonId: null,
|
|
792
1024
|
titles: [linkTitle(linkOf.get(`${u.personAId}>${c}`))],
|
|
793
1025
|
lineStyle: "plain",
|
|
1026
|
+
annotated: aLinkAnnotated(c),
|
|
794
1027
|
build: () => {
|
|
795
1028
|
const sy = geo.corridorLaneY(sibReq);
|
|
796
1029
|
const sx = geo.gutterCenterX(interGutter);
|
|
@@ -816,6 +1049,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
816
1049
|
// declared quality word is dropped from the drawn element (FIX C-2 / SPEC inv #4).
|
|
817
1050
|
titles: [aLinkTitle(groupKids[0]), ...groupKids.slice(2).map(aLinkTitle)],
|
|
818
1051
|
lineStyle: "plain",
|
|
1052
|
+
annotated: [groupKids[0], ...groupKids.slice(2)].some(aLinkAnnotated),
|
|
819
1053
|
build: () => {
|
|
820
1054
|
const y = geo.corridorLaneY(sibReq);
|
|
821
1055
|
const xs = sibSpan();
|
|
@@ -834,6 +1068,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
834
1068
|
titles: [aLinkTitle(groupKids[1])],
|
|
835
1069
|
// the A-side link this spine carries, verbatim
|
|
836
1070
|
lineStyle: "plain",
|
|
1071
|
+
annotated: aLinkAnnotated(groupKids[1]),
|
|
837
1072
|
build: () => {
|
|
838
1073
|
const sy = geo.corridorLaneY(sibReq);
|
|
839
1074
|
const x = geo.gutterCenterX(interGutter);
|
|
@@ -861,22 +1096,24 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
861
1096
|
titles: [linkTitle(l)],
|
|
862
1097
|
lineStyle: "plain",
|
|
863
1098
|
dotted: true,
|
|
1099
|
+
annotated: l.annotated,
|
|
864
1100
|
build
|
|
865
1101
|
});
|
|
866
1102
|
}
|
|
867
1103
|
const bondGroups = /* @__PURE__ */ new Map();
|
|
868
|
-
for (const
|
|
869
|
-
const style = qualityLineStyle(
|
|
870
|
-
const lo = Math.min(
|
|
871
|
-
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);
|
|
872
1108
|
const key = `${lo}|${hi}|${style}`;
|
|
873
|
-
const title =
|
|
1109
|
+
const title = r2.quality !== null ? `${r2.type} \xB7 ${r2.quality}` : r2.type;
|
|
874
1110
|
const g = bondGroups.get(key);
|
|
875
1111
|
if (g === void 0) {
|
|
876
|
-
bondGroups.set(key, { relIds: [
|
|
1112
|
+
bondGroups.set(key, { relIds: [r2.id], titles: [title], style, aId: r2.fromPersonId, bId: r2.toPersonId, annotated: r2.annotated ?? false });
|
|
877
1113
|
} else {
|
|
878
|
-
g.relIds.push(
|
|
1114
|
+
g.relIds.push(r2.id);
|
|
879
1115
|
g.titles.push(title);
|
|
1116
|
+
if (r2.annotated) g.annotated = true;
|
|
880
1117
|
}
|
|
881
1118
|
}
|
|
882
1119
|
const bondList = [...bondGroups.values()].sort((a, b) => Math.max(...a.relIds) - Math.max(...b.relIds));
|
|
@@ -895,6 +1132,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
895
1132
|
toPersonId: g.bId,
|
|
896
1133
|
titles: g.titles,
|
|
897
1134
|
lineStyle: g.style,
|
|
1135
|
+
annotated: g.annotated,
|
|
898
1136
|
build
|
|
899
1137
|
});
|
|
900
1138
|
}
|
|
@@ -910,7 +1148,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
910
1148
|
const laneCount = (gutter, side) => gutterLaneCount.get(`${gutter}:${side}`) ?? 0;
|
|
911
1149
|
const geo = {
|
|
912
1150
|
cx: (c) => colCenterX[c],
|
|
913
|
-
cy: (
|
|
1151
|
+
cy: (r2) => rowCenterY[r2],
|
|
914
1152
|
glyphRight: (id) => colCenterX[colOrThrow(id)] + NODE_SIZE / 2,
|
|
915
1153
|
glyphLeft: (id) => colCenterX[colOrThrow(id)] - NODE_SIZE / 2,
|
|
916
1154
|
gutterCenterX: (g) => {
|
|
@@ -928,7 +1166,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
928
1166
|
},
|
|
929
1167
|
corridorLaneY: (req) => corridorTopY[req.corridor] + CORRIDOR_BASE / 2 + req.lane * LANE_H,
|
|
930
1168
|
/** Y of a dipped serial-union bar (level ≥ 1): below the row's glyph bottom edge. */
|
|
931
|
-
unionDipY: (
|
|
1169
|
+
unionDipY: (r2, level) => rowCenterY[r2] + NODE_SIZE / 2 + level * UNION_STAGGER
|
|
932
1170
|
};
|
|
933
1171
|
const gutterGroups = /* @__PURE__ */ new Map();
|
|
934
1172
|
for (const req of gutterReqs) {
|
|
@@ -938,7 +1176,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
938
1176
|
for (const [k, reqs] of gutterGroups) {
|
|
939
1177
|
gutterLaneCount.set(
|
|
940
1178
|
k,
|
|
941
|
-
|
|
1179
|
+
allocateLanes2(reqs.map((req) => ({ lo: req.rowLo, hi: req.rowHi, set: (lane) => req.lane = lane })))
|
|
942
1180
|
);
|
|
943
1181
|
}
|
|
944
1182
|
const gutterWidth = (g) => {
|
|
@@ -959,7 +1197,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
959
1197
|
const corridorGroups = /* @__PURE__ */ new Map();
|
|
960
1198
|
for (const req of corridorReqs) (corridorGroups.get(req.corridor) ?? corridorGroups.set(req.corridor, []).get(req.corridor)).push(req);
|
|
961
1199
|
for (const [k, reqs] of corridorGroups) {
|
|
962
|
-
corridorLaneCount[k] =
|
|
1200
|
+
corridorLaneCount[k] = allocateLanes2(
|
|
963
1201
|
reqs.map((req) => {
|
|
964
1202
|
const [lo, hi] = req.xRange();
|
|
965
1203
|
return { lo, hi, set: (lane) => req.lane = lane };
|
|
@@ -971,31 +1209,32 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
971
1209
|
if (lanes > 0) return CORRIDOR_BASE + lanes * LANE_H;
|
|
972
1210
|
return k < rowCount - 1 ? CORRIDOR_BASE : 0;
|
|
973
1211
|
};
|
|
974
|
-
const rowDipReserve = (
|
|
1212
|
+
const rowDipReserve = (r2) => rowDipLevels[r2] > 0 ? rowDipLevels[r2] * UNION_STAGGER + 6 : 0;
|
|
975
1213
|
let cursorY = PADDING;
|
|
976
|
-
for (let
|
|
977
|
-
rowCenterY[
|
|
978
|
-
rowLabelTopY[
|
|
979
|
-
cursorY = rowLabelTopY[
|
|
980
|
-
corridorTopY[
|
|
981
|
-
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);
|
|
982
1220
|
}
|
|
983
1221
|
const width = dims.width;
|
|
984
1222
|
const height = cursorY + PADDING;
|
|
985
1223
|
const nodes = [...people].sort((a, b) => a.id - b.id).map((p) => {
|
|
986
1224
|
const m = measured.get(p.id);
|
|
987
|
-
const
|
|
1225
|
+
const r2 = rowOfPerson.get(p.id);
|
|
988
1226
|
const c = colOrThrow(p.id);
|
|
989
1227
|
return {
|
|
990
1228
|
personId: p.id,
|
|
991
1229
|
label: p.label,
|
|
992
1230
|
shape: shapeForSex(p.sex),
|
|
993
1231
|
deceased: p.deceased,
|
|
1232
|
+
annotated: p.annotated ?? false,
|
|
994
1233
|
cx: colCenterX[c],
|
|
995
|
-
cy: rowCenterY[
|
|
1234
|
+
cy: rowCenterY[r2],
|
|
996
1235
|
size: NODE_SIZE,
|
|
997
1236
|
labelLines: m.lines,
|
|
998
|
-
labelTop: rowLabelTopY[
|
|
1237
|
+
labelTop: rowLabelTopY[r2]
|
|
999
1238
|
};
|
|
1000
1239
|
});
|
|
1001
1240
|
const elements = planned.map((pl) => ({
|
|
@@ -1007,6 +1246,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
1007
1246
|
points: pl.build(),
|
|
1008
1247
|
titles: pl.titles,
|
|
1009
1248
|
lineStyle: pl.lineStyle,
|
|
1249
|
+
annotated: pl.annotated,
|
|
1010
1250
|
...pl.unionStyle !== void 0 ? { unionStyle: pl.unionStyle } : {},
|
|
1011
1251
|
...pl.dotted ? { dotted: true } : {}
|
|
1012
1252
|
}));
|
|
@@ -1111,6 +1351,7 @@ function elementSvg(el, override) {
|
|
|
1111
1351
|
}
|
|
1112
1352
|
return `<path d="${pathData2(pts)}" fill="none" stroke="${EDGE_INK}" stroke-width="${width}" stroke-opacity="${opacity}"${dashAttr}/>`;
|
|
1113
1353
|
};
|
|
1354
|
+
const tick = el.annotated ? annotationTick(el.points) : "";
|
|
1114
1355
|
if (el.kind === "union-bar" || el.kind === "union-elbow") {
|
|
1115
1356
|
const style = resolveUnionStyle(el, override);
|
|
1116
1357
|
const body = [drawLine(style.dash ?? null, STRUCT_WIDTH, STRUCT_OPACITY)];
|
|
@@ -1119,15 +1360,15 @@ function elementSvg(el, override) {
|
|
|
1119
1360
|
const [a, b] = pts.length === 2 ? [pts[0], pts[1]] : longestHSegment(pts);
|
|
1120
1361
|
body.push(slashMarks(a, b, slashes, STRUCT_WIDTH));
|
|
1121
1362
|
}
|
|
1122
|
-
return `<g data-edge-id="${el.edgeId}">${title}${body.join("")}</g>`;
|
|
1363
|
+
return `<g data-edge-id="${el.edgeId}">${title}${body.join("")}${tick}</g>`;
|
|
1123
1364
|
}
|
|
1124
1365
|
if (el.kind === "descent" || el.kind === "sibling-bar") {
|
|
1125
1366
|
const dash = el.dotted ? DOTTED_DASH : null;
|
|
1126
1367
|
const opacity = el.dotted ? DOTTED_OPACITY : STRUCT_OPACITY;
|
|
1127
|
-
return `<g data-edge-id="${el.edgeId}">${title}${drawLine(dash, STRUCT_WIDTH, opacity)}</g>`;
|
|
1368
|
+
return `<g data-edge-id="${el.edgeId}">${title}${drawLine(dash, STRUCT_WIDTH, opacity)}${tick}</g>`;
|
|
1128
1369
|
}
|
|
1129
1370
|
const ink = EDGE_STROKE[el.lineStyle];
|
|
1130
|
-
return `<g data-edge-id="${el.edgeId}">${title}${drawLine(ink.dash, ink.width, ink.opacity)}</g>`;
|
|
1371
|
+
return `<g data-edge-id="${el.edgeId}">${title}${drawLine(ink.dash, ink.width, ink.opacity)}${tick}</g>`;
|
|
1131
1372
|
}
|
|
1132
1373
|
function genogramLayoutSvg(layout, opts = {}) {
|
|
1133
1374
|
const override = opts.unionStyleByRelId ?? /* @__PURE__ */ new Map();
|
|
@@ -1145,6 +1386,9 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1145
1386
|
`<line x1="${node.cx - half}" y1="${node.cy - half}" x2="${node.cx + half}" y2="${node.cy + half}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`
|
|
1146
1387
|
);
|
|
1147
1388
|
}
|
|
1389
|
+
if (node.annotated) {
|
|
1390
|
+
pieces.push(annotationDot(node.cx + 0.7 * half, node.cy - 0.7 * half));
|
|
1391
|
+
}
|
|
1148
1392
|
const tspans = node.labelLines.map(
|
|
1149
1393
|
(line, i) => `<tspan x="${node.cx}" y="${node.labelTop + 10 + i * LABEL_LINE_H}">${xmlEscape(line)}</tspan>`
|
|
1150
1394
|
).join("");
|
|
@@ -1187,6 +1431,12 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1187
1431
|
label: labels.isolated
|
|
1188
1432
|
});
|
|
1189
1433
|
}
|
|
1434
|
+
if (opts.annotationLabel !== void 0 && (layout.nodes.some((n) => n.annotated) || layout.elements.some((e) => e.annotated))) {
|
|
1435
|
+
entries.push({
|
|
1436
|
+
swatch: (x, y) => annotationSwatch(x, y),
|
|
1437
|
+
label: opts.annotationLabel
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1190
1440
|
const block = legendBlock(entries, layout.height);
|
|
1191
1441
|
if (block.svg !== "") {
|
|
1192
1442
|
parts.push(block.svg);
|
|
@@ -1238,7 +1488,8 @@ function genogramSvg(input, opts = {}) {
|
|
|
1238
1488
|
const svg = genogramLayoutSvg(layout, {
|
|
1239
1489
|
unionStyleByRelId,
|
|
1240
1490
|
...opts.legend === false ? { legend: false } : {},
|
|
1241
|
-
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
1491
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {},
|
|
1492
|
+
...opts.annotationLabel !== void 0 ? { annotationLabel: opts.annotationLabel } : {}
|
|
1242
1493
|
});
|
|
1243
1494
|
return { svg, layout };
|
|
1244
1495
|
}
|
|
@@ -1268,7 +1519,7 @@ var SINGLE_RING_MAX = 8;
|
|
|
1268
1519
|
var NODE_STROKE = "#52525b";
|
|
1269
1520
|
var LABEL_FILL = "#3f3f46";
|
|
1270
1521
|
var EDGE_INK2 = "#71717a";
|
|
1271
|
-
var
|
|
1522
|
+
var round4 = (n) => Math.round(n * 100) / 100;
|
|
1272
1523
|
function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
1273
1524
|
const LEN = 9;
|
|
1274
1525
|
const HALF_W = 4.5;
|
|
@@ -1277,9 +1528,9 @@ function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
|
1277
1528
|
const px = -uy;
|
|
1278
1529
|
const py = ux;
|
|
1279
1530
|
const points = [
|
|
1280
|
-
`${
|
|
1281
|
-
`${
|
|
1282
|
-
`${
|
|
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)}`
|
|
1283
1534
|
].join(" ");
|
|
1284
1535
|
return `<polygon points="${points}" fill="${EDGE_INK2}" fill-opacity="${opacity}"/>`;
|
|
1285
1536
|
}
|
|
@@ -1325,16 +1576,16 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1325
1576
|
sameRingRadius(twoRings ? 2 : 1)
|
|
1326
1577
|
);
|
|
1327
1578
|
if (twoRings) {
|
|
1328
|
-
const crossDist = (
|
|
1579
|
+
const crossDist = (r2) => Math.sqrt(r2 * r2 + (r2 + ringStep) ** 2 - 2 * r2 * (r2 + ringStep) * Math.cos(stepAngle));
|
|
1329
1580
|
while (crossDist(innerR) < safeDist) innerR += 8;
|
|
1330
1581
|
}
|
|
1331
1582
|
const outerR = innerR + ringStep;
|
|
1332
1583
|
for (let i = 0; i < n; i++) {
|
|
1333
1584
|
const s = sats[i];
|
|
1334
1585
|
s.angle = -Math.PI / 2 + i * Math.PI * 2 / n;
|
|
1335
|
-
const
|
|
1336
|
-
s.x =
|
|
1337
|
-
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);
|
|
1338
1589
|
}
|
|
1339
1590
|
let minX = -centerR;
|
|
1340
1591
|
let minY = -centerR;
|
|
@@ -1369,7 +1620,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1369
1620
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1370
1621
|
const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} \xB7 ${s.tie.quality}` : s.tie.label);
|
|
1371
1622
|
const body = [
|
|
1372
|
-
`<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}/>`
|
|
1373
1624
|
];
|
|
1374
1625
|
if (s.tie.direction === "in" || s.tie.direction === "both") {
|
|
1375
1626
|
body.push(arrowHead(x2, y2, ux, uy, ink.opacity));
|
|
@@ -1381,19 +1632,25 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1381
1632
|
}
|
|
1382
1633
|
{
|
|
1383
1634
|
const tspans = centerLines.map(
|
|
1384
|
-
(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>`
|
|
1385
1636
|
).join("");
|
|
1386
1637
|
parts.push(
|
|
1387
|
-
`<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>`
|
|
1388
1639
|
);
|
|
1389
1640
|
}
|
|
1390
1641
|
for (const s of sats) {
|
|
1391
1642
|
const tspans = s.lines.map(
|
|
1392
|
-
(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>`
|
|
1393
1644
|
).join("");
|
|
1394
|
-
|
|
1395
|
-
`<
|
|
1396
|
-
|
|
1645
|
+
const pieces = [
|
|
1646
|
+
`<title>${xmlEscape(s.tie.label)}</title>`,
|
|
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"/>`,
|
|
1648
|
+
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text>`
|
|
1649
|
+
];
|
|
1650
|
+
if (s.tie.annotated) {
|
|
1651
|
+
pieces.push(annotationDot(s.x + 0.7 * s.rx, s.y - 0.7 * s.ry));
|
|
1652
|
+
}
|
|
1653
|
+
parts.push(`<g data-individual-id="e${s.tie.id}">${pieces.join("")}</g>`);
|
|
1397
1654
|
}
|
|
1398
1655
|
if (opts.legend !== false && sats.length > 0) {
|
|
1399
1656
|
const entries = [];
|
|
@@ -1419,6 +1676,12 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1419
1676
|
label: labels.direction
|
|
1420
1677
|
});
|
|
1421
1678
|
}
|
|
1679
|
+
if (opts.annotationLabel !== void 0 && sats.some((s) => s.tie.annotated)) {
|
|
1680
|
+
entries.push({
|
|
1681
|
+
swatch: (x, y) => annotationSwatch(x, y),
|
|
1682
|
+
label: opts.annotationLabel
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1422
1685
|
if (entries.length > 0) {
|
|
1423
1686
|
const block = legendBlock(entries, height);
|
|
1424
1687
|
parts.push(block.svg);
|
|
@@ -1592,7 +1855,7 @@ function faultTreeIssues(input) {
|
|
|
1592
1855
|
);
|
|
1593
1856
|
}
|
|
1594
1857
|
}
|
|
1595
|
-
const
|
|
1858
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
1596
1859
|
"duplicate-id",
|
|
1597
1860
|
"unknown-input",
|
|
1598
1861
|
"gate-on-non-intermediate",
|
|
@@ -1601,7 +1864,7 @@ function faultTreeIssues(input) {
|
|
|
1601
1864
|
"inhibit-condition",
|
|
1602
1865
|
"top-not-intermediate"
|
|
1603
1866
|
]);
|
|
1604
|
-
if (!issues.some((i) =>
|
|
1867
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
1605
1868
|
const gateOf = (eventId) => gates.find((g) => g.eventId === eventId);
|
|
1606
1869
|
const reachable = /* @__PURE__ */ new Set();
|
|
1607
1870
|
const queue = [input.topId];
|
|
@@ -1681,7 +1944,7 @@ var FT_DROP_ID_BASE = 2e6;
|
|
|
1681
1944
|
var FT_BUS_ID_BASE = 3e6;
|
|
1682
1945
|
var FT_RISER_ID_BASE = 4e6;
|
|
1683
1946
|
var FT_CONDITION_ID_BASE = 5e6;
|
|
1684
|
-
var
|
|
1947
|
+
var round5 = (n) => Math.round(n * 100) / 100;
|
|
1685
1948
|
function wrapLeafLabel(displayLabel) {
|
|
1686
1949
|
const perLine = Math.min(20, Math.max(12, Math.ceil(displayLabel.length / 2) + 2));
|
|
1687
1950
|
return wrapLabel(displayLabel, perLine);
|
|
@@ -1868,14 +2131,14 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1868
2131
|
eventId: inst.event.id,
|
|
1869
2132
|
kind: inst.event.kind,
|
|
1870
2133
|
instance: instanceOf(inst),
|
|
1871
|
-
cx:
|
|
1872
|
-
top:
|
|
1873
|
-
nodeW:
|
|
1874
|
-
nodeH:
|
|
1875
|
-
glyphW:
|
|
1876
|
-
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),
|
|
1877
2140
|
labelLines: inst.m.labelLines,
|
|
1878
|
-
labelTop: labelTop === null ? null :
|
|
2141
|
+
labelTop: labelTop === null ? null : round5(labelTop),
|
|
1879
2142
|
code: inst.m.code,
|
|
1880
2143
|
depth: inst.depth,
|
|
1881
2144
|
title: inst.title
|
|
@@ -1893,8 +2156,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1893
2156
|
gates.push({
|
|
1894
2157
|
gateId: g.id,
|
|
1895
2158
|
type: g.type,
|
|
1896
|
-
cx:
|
|
1897
|
-
top:
|
|
2159
|
+
cx: round5(inst.cx),
|
|
2160
|
+
top: round5(gateTop),
|
|
1898
2161
|
glyphW,
|
|
1899
2162
|
glyphH,
|
|
1900
2163
|
title,
|
|
@@ -1911,8 +2174,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1911
2174
|
edgeId: FT_CONDITION_ID_BASE + g.id,
|
|
1912
2175
|
kind: "condition",
|
|
1913
2176
|
points: [
|
|
1914
|
-
{ x:
|
|
1915
|
-
{ x:
|
|
2177
|
+
{ x: round5(inst.cx + INHIBIT_W / 2), y: round5(gateTop + INHIBIT_CY) },
|
|
2178
|
+
{ x: round5(ovalLeft), y: round5(gateTop + INHIBIT_CY) }
|
|
1916
2179
|
],
|
|
1917
2180
|
instance: null,
|
|
1918
2181
|
title: titleLabels.condition
|
|
@@ -1922,8 +2185,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1922
2185
|
edgeId: FT_STEM_ID_BASE + g.id,
|
|
1923
2186
|
kind: "stem",
|
|
1924
2187
|
points: [
|
|
1925
|
-
{ x:
|
|
1926
|
-
{ x:
|
|
2188
|
+
{ x: round5(inst.cx), y: round5(rowTop[d] + inst.m.nodeH) },
|
|
2189
|
+
{ x: round5(inst.cx), y: round5(gateTop) }
|
|
1927
2190
|
],
|
|
1928
2191
|
instance: null,
|
|
1929
2192
|
title
|
|
@@ -1933,8 +2196,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1933
2196
|
edgeId: FT_DROP_ID_BASE + g.id,
|
|
1934
2197
|
kind: "drop",
|
|
1935
2198
|
points: [
|
|
1936
|
-
{ x:
|
|
1937
|
-
{ x:
|
|
2199
|
+
{ x: round5(inst.cx), y: round5(gateTop + glyphH) },
|
|
2200
|
+
{ x: round5(inst.cx), y: round5(by) }
|
|
1938
2201
|
],
|
|
1939
2202
|
instance: null,
|
|
1940
2203
|
title
|
|
@@ -1945,8 +2208,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1945
2208
|
edgeId: FT_BUS_ID_BASE + g.id,
|
|
1946
2209
|
kind: "bus",
|
|
1947
2210
|
points: [
|
|
1948
|
-
{ x:
|
|
1949
|
-
{ x:
|
|
2211
|
+
{ x: round5(Math.min(...xs)), y: round5(by) },
|
|
2212
|
+
{ x: round5(Math.max(...xs)), y: round5(by) }
|
|
1950
2213
|
],
|
|
1951
2214
|
instance: null,
|
|
1952
2215
|
title
|
|
@@ -1957,8 +2220,8 @@ function computeFaultTreeLayout(input, opts = {}) {
|
|
|
1957
2220
|
edgeId: FT_RISER_ID_BASE + c.event.id,
|
|
1958
2221
|
kind: "riser",
|
|
1959
2222
|
points: [
|
|
1960
|
-
{ x:
|
|
1961
|
-
{ x:
|
|
2223
|
+
{ x: round5(c.cx), y: round5(by) },
|
|
2224
|
+
{ x: round5(c.cx), y: round5(rowTop[d + 1]) }
|
|
1962
2225
|
],
|
|
1963
2226
|
instance: instanceOf(c),
|
|
1964
2227
|
title
|
|
@@ -1975,42 +2238,42 @@ var GLYPH_STROKE2 = "#52525b";
|
|
|
1975
2238
|
var LABEL_FILL2 = "#3f3f46";
|
|
1976
2239
|
var EDGE_INK3 = "#71717a";
|
|
1977
2240
|
var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="2"`;
|
|
1978
|
-
var
|
|
2241
|
+
var round6 = (n) => Math.round(n * 100) / 100;
|
|
1979
2242
|
function eventGlyph(n) {
|
|
1980
2243
|
const cx = n.cx;
|
|
1981
2244
|
const top = n.top;
|
|
1982
2245
|
if (n.kind === "intermediate") {
|
|
1983
|
-
return `<rect x="${
|
|
2246
|
+
return `<rect x="${round6(cx - n.nodeW / 2)}" y="${top}" width="${n.nodeW}" height="${n.nodeH}" rx="2" ${GLYPH_ATTRS}/>`;
|
|
1984
2247
|
}
|
|
1985
2248
|
if (n.kind === "basic") {
|
|
1986
|
-
return `<circle cx="${cx}" cy="${
|
|
2249
|
+
return `<circle cx="${cx}" cy="${round6(top + 22)}" r="22" ${GLYPH_ATTRS}/>`;
|
|
1987
2250
|
}
|
|
1988
2251
|
if (n.kind === "undeveloped") {
|
|
1989
|
-
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)}`;
|
|
1990
2253
|
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
1991
2254
|
}
|
|
1992
2255
|
if (n.kind === "house") {
|
|
1993
|
-
const yB =
|
|
1994
|
-
const eave =
|
|
1995
|
-
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}`;
|
|
1996
2259
|
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
1997
2260
|
}
|
|
1998
2261
|
if (n.kind === "conditioning") {
|
|
1999
|
-
return `<ellipse cx="${cx}" cy="${
|
|
2262
|
+
return `<ellipse cx="${cx}" cy="${round6(top + 16)}" rx="${round6(n.glyphW / 2)}" ry="16" ${GLYPH_ATTRS}/>`;
|
|
2000
2263
|
}
|
|
2001
|
-
const pts = `${cx},${top} ${
|
|
2264
|
+
const pts = `${cx},${top} ${round6(cx + 22)},${round6(top + 35)} ${round6(cx - 22)},${round6(top + 35)}`;
|
|
2002
2265
|
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2003
2266
|
}
|
|
2004
2267
|
function nodeSvg(n) {
|
|
2005
2268
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`, eventGlyph(n)];
|
|
2006
2269
|
if (n.code !== null && n.kind !== "intermediate") {
|
|
2007
2270
|
pieces.push(
|
|
2008
|
-
`<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>`
|
|
2009
2272
|
);
|
|
2010
2273
|
}
|
|
2011
2274
|
if (n.labelLines.length > 0) {
|
|
2012
|
-
const firstBaseline = n.labelTop === null ?
|
|
2013
|
-
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("");
|
|
2014
2277
|
pieces.push(
|
|
2015
2278
|
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${FT_LABEL_FONT}" fill="${LABEL_FILL2}">${tspans}</text>`
|
|
2016
2279
|
);
|
|
@@ -2019,28 +2282,28 @@ function nodeSvg(n) {
|
|
|
2019
2282
|
return `<g data-node-id="e${n.eventId}"${instance}>${pieces.join("")}</g>`;
|
|
2020
2283
|
}
|
|
2021
2284
|
function orBodyPath(cx, top, yB) {
|
|
2022
|
-
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`;
|
|
2023
2286
|
}
|
|
2024
2287
|
function gateGlyph(g) {
|
|
2025
2288
|
const cx = g.cx;
|
|
2026
2289
|
const top = g.top;
|
|
2027
|
-
const yB =
|
|
2290
|
+
const yB = round6(top + 36);
|
|
2028
2291
|
if (g.type === "and") {
|
|
2029
|
-
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`;
|
|
2030
2293
|
return `<path d="${d}" ${GLYPH_ATTRS}/>`;
|
|
2031
2294
|
}
|
|
2032
2295
|
if (g.type === "or") {
|
|
2033
2296
|
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/>`;
|
|
2034
2297
|
}
|
|
2035
2298
|
if (g.type === "xor") {
|
|
2036
|
-
const arc = `M ${
|
|
2299
|
+
const arc = `M ${round6(cx - 22)} ${round6(yB + 5)} Q ${cx} ${round6(yB - 9)} ${round6(cx + 22)} ${round6(yB + 5)}`;
|
|
2037
2300
|
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><path d="${arc}" ${GLYPH_ATTRS}/>`;
|
|
2038
2301
|
}
|
|
2039
2302
|
if (g.type === "inhibit") {
|
|
2040
|
-
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)}`;
|
|
2041
2304
|
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2042
2305
|
}
|
|
2043
|
-
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>`;
|
|
2044
2307
|
}
|
|
2045
2308
|
function gateSvg(g) {
|
|
2046
2309
|
return `<g data-node-id="g${g.gateId}"><title>${xmlEscape(g.title)}</title>${gateGlyph(g)}</g>`;
|
|
@@ -2053,34 +2316,34 @@ function elementSvg2(el) {
|
|
|
2053
2316
|
}
|
|
2054
2317
|
var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="1.5"`;
|
|
2055
2318
|
function miniEventSwatch(kind, x, y) {
|
|
2056
|
-
const cx =
|
|
2319
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
2057
2320
|
if (kind === "intermediate") {
|
|
2058
|
-
return `<rect x="${
|
|
2321
|
+
return `<rect x="${round6(cx - 7)}" y="${round6(y - 4.5)}" width="14" height="9" rx="1" ${MINI_ATTRS}/>`;
|
|
2059
2322
|
}
|
|
2060
2323
|
if (kind === "basic") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/>`;
|
|
2061
2324
|
if (kind === "undeveloped") {
|
|
2062
|
-
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}/>`;
|
|
2063
2326
|
}
|
|
2064
2327
|
if (kind === "house") {
|
|
2065
|
-
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}/>`;
|
|
2066
2329
|
}
|
|
2067
2330
|
if (kind === "conditioning") return `<ellipse cx="${cx}" cy="${y}" rx="9" ry="5.5" ${MINI_ATTRS}/>`;
|
|
2068
|
-
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}/>`;
|
|
2069
2332
|
}
|
|
2070
2333
|
function miniOrPath(cx, y) {
|
|
2071
|
-
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`;
|
|
2072
2335
|
}
|
|
2073
2336
|
function miniGateSwatch(type, x, y) {
|
|
2074
|
-
const cx =
|
|
2337
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
2075
2338
|
if (type === "and") {
|
|
2076
|
-
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`;
|
|
2077
2340
|
return `<path d="${d}" ${MINI_ATTRS}/>`;
|
|
2078
2341
|
}
|
|
2079
2342
|
if (type === "xor") {
|
|
2080
|
-
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}/>`;
|
|
2081
2344
|
}
|
|
2082
2345
|
if (type === "inhibit") {
|
|
2083
|
-
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}/>`;
|
|
2084
2347
|
}
|
|
2085
2348
|
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/>`;
|
|
2086
2349
|
}
|
|
@@ -2209,7 +2472,7 @@ var TWIG_W = 1.5;
|
|
|
2209
2472
|
var TWIG_OP = 0.75;
|
|
2210
2473
|
var SUB_W = 1.2;
|
|
2211
2474
|
var SUB_OP = 0.7;
|
|
2212
|
-
var
|
|
2475
|
+
var round7 = (n) => Math.round(n * 100) / 100;
|
|
2213
2476
|
function arrowHead2(tipX, tipY, ux, uy, opacity) {
|
|
2214
2477
|
const LEN = 9;
|
|
2215
2478
|
const HALF_W = 4.5;
|
|
@@ -2218,14 +2481,14 @@ function arrowHead2(tipX, tipY, ux, uy, opacity) {
|
|
|
2218
2481
|
const px = -uy;
|
|
2219
2482
|
const py = ux;
|
|
2220
2483
|
const points = [
|
|
2221
|
-
`${
|
|
2222
|
-
`${
|
|
2223
|
-
`${
|
|
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)}`
|
|
2224
2487
|
].join(" ");
|
|
2225
2488
|
return `<polygon points="${points}" fill="${EDGE_INK4}" fill-opacity="${opacity}"/>`;
|
|
2226
2489
|
}
|
|
2227
2490
|
function lineEl(x1, y1, x2, y2, w, op) {
|
|
2228
|
-
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}"/>`;
|
|
2229
2492
|
}
|
|
2230
2493
|
function fishboneSvg(input, opts = {}) {
|
|
2231
2494
|
validateIds(input);
|
|
@@ -2306,7 +2569,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2306
2569
|
{ length: n },
|
|
2307
2570
|
(_, k) => up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH
|
|
2308
2571
|
);
|
|
2309
|
-
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>`;
|
|
2310
2573
|
const parts = [];
|
|
2311
2574
|
{
|
|
2312
2575
|
const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];
|
|
@@ -2321,7 +2584,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2321
2584
|
if (arrows) body.push(arrowHead2(ax, spineY, COS60, -sgn * SIN60, BONE_OP));
|
|
2322
2585
|
const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;
|
|
2323
2586
|
body.push(
|
|
2324
|
-
`<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"/>`
|
|
2325
2588
|
);
|
|
2326
2589
|
body.push(textBlock("middle", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));
|
|
2327
2590
|
parts.push(
|
|
@@ -2351,7 +2614,7 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2351
2614
|
{
|
|
2352
2615
|
const x = headLeft + dx;
|
|
2353
2616
|
parts.push(
|
|
2354
|
-
`<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>`
|
|
2355
2618
|
);
|
|
2356
2619
|
}
|
|
2357
2620
|
const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));
|
|
@@ -2565,13 +2828,13 @@ function pedigreeIssues(input) {
|
|
|
2565
2828
|
);
|
|
2566
2829
|
}
|
|
2567
2830
|
}
|
|
2568
|
-
const
|
|
2831
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
2569
2832
|
"duplicate-id",
|
|
2570
2833
|
"unknown-partner",
|
|
2571
2834
|
"unknown-sibship-mating",
|
|
2572
2835
|
"unknown-child"
|
|
2573
2836
|
]);
|
|
2574
|
-
if (!issues.some((i) =>
|
|
2837
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
2575
2838
|
for (const s of sibships) {
|
|
2576
2839
|
const mating = matingById.get(s.matingId);
|
|
2577
2840
|
if (mating === void 0) continue;
|
|
@@ -2619,7 +2882,7 @@ var PED_DESCENT_ID_BASE = 2e6;
|
|
|
2619
2882
|
var PED_SIBBAR_ID_BASE = 3e6;
|
|
2620
2883
|
var PED_RISER_ID_BASE = 4e6;
|
|
2621
2884
|
var PED_TWINBAR_ID_BASE = 5e6;
|
|
2622
|
-
var
|
|
2885
|
+
var round8 = (n) => Math.round(n * 100) / 100;
|
|
2623
2886
|
function shapeForSex2(sex) {
|
|
2624
2887
|
if (sex === "male") return "square";
|
|
2625
2888
|
if (sex === "female") return "circle";
|
|
@@ -2858,8 +3121,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2858
3121
|
rowHeight[row] = Math.max(rowHeight[row], h);
|
|
2859
3122
|
}
|
|
2860
3123
|
const rowTop = [PADDING5];
|
|
2861
|
-
for (let
|
|
2862
|
-
rowTop.push(rowTop[
|
|
3124
|
+
for (let r2 = 0; r2 < rowCount - 1; r2++) {
|
|
3125
|
+
rowTop.push(rowTop[r2] + rowHeight[r2] + CORRIDOR2);
|
|
2863
3126
|
}
|
|
2864
3127
|
const glyphCyOfRow = (row) => rowTop[row] + PED_GLYPH / 2;
|
|
2865
3128
|
const addressByIndividual = /* @__PURE__ */ new Map();
|
|
@@ -2885,8 +3148,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2885
3148
|
nodes.push({
|
|
2886
3149
|
individualId: ind.id,
|
|
2887
3150
|
shape: shapeForSex2(ind.sex),
|
|
2888
|
-
cx:
|
|
2889
|
-
cy:
|
|
3151
|
+
cx: round8(placed.cx),
|
|
3152
|
+
cy: round8(cy),
|
|
2890
3153
|
size: PED_GLYPH,
|
|
2891
3154
|
deceased: ind.deceased,
|
|
2892
3155
|
carrier: ind.carrier,
|
|
@@ -2896,7 +3159,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2896
3159
|
labelLines: lines,
|
|
2897
3160
|
// Labels sit BELOW the row's serial-union dip band (rowDipBand) so a hub's bridge dips
|
|
2898
3161
|
// never cross a label box.
|
|
2899
|
-
labelTop:
|
|
3162
|
+
labelTop: round8(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
|
|
2900
3163
|
addressLabel: address,
|
|
2901
3164
|
title: individualTitle(ind, address, conditionLabelById, titleLabels)
|
|
2902
3165
|
});
|
|
@@ -2939,23 +3202,23 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2939
3202
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2940
3203
|
kind: "mating-elbow",
|
|
2941
3204
|
points: [
|
|
2942
|
-
{ x:
|
|
2943
|
-
{ x:
|
|
2944
|
-
{ x:
|
|
2945
|
-
{ 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) }
|
|
2946
3209
|
],
|
|
2947
3210
|
consanguineous: m.consanguineous,
|
|
2948
3211
|
title
|
|
2949
3212
|
});
|
|
2950
|
-
matingMidpoint.set(m.id, { x:
|
|
3213
|
+
matingMidpoint.set(m.id, { x: round8((hubStubX + spouseStubX) / 2), y: round8(dipY) });
|
|
2951
3214
|
} else if (ay === by) {
|
|
2952
3215
|
const y = ay;
|
|
2953
3216
|
elements.push({
|
|
2954
3217
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2955
3218
|
kind: "mating",
|
|
2956
3219
|
points: [
|
|
2957
|
-
{ x:
|
|
2958
|
-
{ x:
|
|
3220
|
+
{ x: round8(lx), y: round8(y) },
|
|
3221
|
+
{ x: round8(rx), y: round8(y) }
|
|
2959
3222
|
],
|
|
2960
3223
|
consanguineous: m.consanguineous,
|
|
2961
3224
|
title
|
|
@@ -2965,7 +3228,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2965
3228
|
const channelRight = cxOf(rightId) - nodeHalfWidth(individualById.get(rightId));
|
|
2966
3229
|
const barCenter = matingBarCenter.get(m.id);
|
|
2967
3230
|
const originX = barCenter !== void 0 && barCenter >= channelLeft && barCenter <= channelRight ? barCenter : midX;
|
|
2968
|
-
matingMidpoint.set(m.id, { x:
|
|
3231
|
+
matingMidpoint.set(m.id, { x: round8(originX), y: round8(y) });
|
|
2969
3232
|
} else {
|
|
2970
3233
|
const upId = ay <= by ? aId : bId;
|
|
2971
3234
|
const downId = ay <= by ? bId : aId;
|
|
@@ -2984,32 +3247,32 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
2984
3247
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2985
3248
|
kind: "mating-elbow",
|
|
2986
3249
|
points: [
|
|
2987
|
-
{ x:
|
|
2988
|
-
{ x:
|
|
2989
|
-
{ x:
|
|
2990
|
-
{ x:
|
|
2991
|
-
{ x:
|
|
2992
|
-
{ 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) }
|
|
2993
3256
|
],
|
|
2994
3257
|
consanguineous: m.consanguineous,
|
|
2995
3258
|
title
|
|
2996
3259
|
});
|
|
2997
|
-
matingMidpoint.set(m.id, { x:
|
|
3260
|
+
matingMidpoint.set(m.id, { x: round8(downFarX), y: round8(downY) });
|
|
2998
3261
|
} else {
|
|
2999
3262
|
const elbowX = (upX + downX) / 2;
|
|
3000
3263
|
elements.push({
|
|
3001
3264
|
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3002
3265
|
kind: "mating-elbow",
|
|
3003
3266
|
points: [
|
|
3004
|
-
{ x:
|
|
3005
|
-
{ x:
|
|
3006
|
-
{ x:
|
|
3007
|
-
{ 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) }
|
|
3008
3271
|
],
|
|
3009
3272
|
consanguineous: m.consanguineous,
|
|
3010
3273
|
title
|
|
3011
3274
|
});
|
|
3012
|
-
matingMidpoint.set(m.id, { x:
|
|
3275
|
+
matingMidpoint.set(m.id, { x: round8(elbowX), y: round8(downY) });
|
|
3013
3276
|
}
|
|
3014
3277
|
}
|
|
3015
3278
|
}
|
|
@@ -3031,13 +3294,13 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3031
3294
|
const barCenterX = (barLeft + barRight) / 2;
|
|
3032
3295
|
const bendY = barY - 2 - lane % 3 * 3;
|
|
3033
3296
|
const descentPoints = Math.abs(mid.x - barCenterX) < 0.01 ? [
|
|
3034
|
-
{ x:
|
|
3035
|
-
{ x:
|
|
3297
|
+
{ x: round8(mid.x), y: round8(mid.y) },
|
|
3298
|
+
{ x: round8(mid.x), y: round8(barY) }
|
|
3036
3299
|
] : [
|
|
3037
|
-
{ x:
|
|
3038
|
-
{ x:
|
|
3039
|
-
{ x:
|
|
3040
|
-
{ 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) }
|
|
3041
3304
|
];
|
|
3042
3305
|
elements.push({
|
|
3043
3306
|
edgeId: PED_DESCENT_ID_BASE + s.id,
|
|
@@ -3052,8 +3315,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3052
3315
|
edgeId: PED_SIBBAR_ID_BASE + s.id,
|
|
3053
3316
|
kind: "sibship-bar",
|
|
3054
3317
|
points: [
|
|
3055
|
-
{ x:
|
|
3056
|
-
{ x:
|
|
3318
|
+
{ x: round8(barLeft), y: round8(barY) },
|
|
3319
|
+
{ x: round8(barRight), y: round8(barY) }
|
|
3057
3320
|
],
|
|
3058
3321
|
consanguineous: false,
|
|
3059
3322
|
title: titleLabels.sibship
|
|
@@ -3074,8 +3337,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3074
3337
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
3075
3338
|
kind: "riser",
|
|
3076
3339
|
points: [
|
|
3077
|
-
{ x:
|
|
3078
|
-
{ x:
|
|
3340
|
+
{ x: round8(cx), y: round8(barY) },
|
|
3341
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3079
3342
|
],
|
|
3080
3343
|
consanguineous: false,
|
|
3081
3344
|
title: titleLabels.sibship
|
|
@@ -3089,15 +3352,15 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3089
3352
|
if (!emittedTwinOrdinals.has(tg.ordinal)) {
|
|
3090
3353
|
emittedTwinOrdinals.add(tg.ordinal);
|
|
3091
3354
|
if (tg.zygosity === "unknown") {
|
|
3092
|
-
unknownTwinJunctions.push({ x:
|
|
3355
|
+
unknownTwinJunctions.push({ x: round8(junctionX), y: round8(junctionY) });
|
|
3093
3356
|
}
|
|
3094
3357
|
elements.push({
|
|
3095
3358
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
3096
3359
|
// anchored on the first member for a stable id
|
|
3097
3360
|
kind: "riser",
|
|
3098
3361
|
points: [
|
|
3099
|
-
{ x:
|
|
3100
|
-
{ x:
|
|
3362
|
+
{ x: round8(junctionX), y: round8(barY) },
|
|
3363
|
+
{ x: round8(junctionX), y: round8(junctionY) }
|
|
3101
3364
|
],
|
|
3102
3365
|
consanguineous: false,
|
|
3103
3366
|
title: titleLabels.twins[tg.zygosity]
|
|
@@ -3108,8 +3371,8 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3108
3371
|
edgeId: PED_TWINBAR_ID_BASE + s.id * 100 + tg.ordinal,
|
|
3109
3372
|
kind: "twin-bar",
|
|
3110
3373
|
points: [
|
|
3111
|
-
{ x:
|
|
3112
|
-
{ x:
|
|
3374
|
+
{ x: round8(Math.min(...memberXs)), y: round8(tieY) },
|
|
3375
|
+
{ x: round8(Math.max(...memberXs)), y: round8(tieY) }
|
|
3113
3376
|
],
|
|
3114
3377
|
consanguineous: false,
|
|
3115
3378
|
title: titleLabels.twins.mz
|
|
@@ -3117,12 +3380,12 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3117
3380
|
}
|
|
3118
3381
|
}
|
|
3119
3382
|
const horizontalThenDown = Math.abs(cx - junctionX) < 0.01 ? [
|
|
3120
|
-
{ x:
|
|
3121
|
-
{ x:
|
|
3383
|
+
{ x: round8(cx), y: round8(junctionY) },
|
|
3384
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3122
3385
|
] : [
|
|
3123
|
-
{ x:
|
|
3124
|
-
{ x:
|
|
3125
|
-
{ x:
|
|
3386
|
+
{ x: round8(junctionX), y: round8(junctionY) },
|
|
3387
|
+
{ x: round8(cx), y: round8(junctionY) },
|
|
3388
|
+
{ x: round8(cx), y: round8(childTop) }
|
|
3126
3389
|
];
|
|
3127
3390
|
elements.push({
|
|
3128
3391
|
edgeId: PED_RISER_ID_BASE + childId,
|
|
@@ -3150,7 +3413,7 @@ function computePedigreeLayout(input, opts = {}) {
|
|
|
3150
3413
|
}
|
|
3151
3414
|
const generations = [];
|
|
3152
3415
|
for (let row = 0; row < rowCount; row++) {
|
|
3153
|
-
generations.push({ roman: romanNumeral(row + 1), y:
|
|
3416
|
+
generations.push({ roman: romanNumeral(row + 1), y: round8(glyphCyOfRow(row)) });
|
|
3154
3417
|
}
|
|
3155
3418
|
const ZYGOSITY_ORDER = ["mz", "dz", "unknown"];
|
|
3156
3419
|
const twinZygositiesUsed = ZYGOSITY_ORDER.filter((z) => twinZygositiesUsedSet.has(z));
|
|
@@ -3174,15 +3437,15 @@ var EDGE_INK5 = "#71717a";
|
|
|
3174
3437
|
var GLYPH_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="2"`;
|
|
3175
3438
|
var CONSANG_GAP = 3;
|
|
3176
3439
|
var ZYGOSITIES = ["mz", "dz", "unknown"];
|
|
3177
|
-
var
|
|
3440
|
+
var round9 = (n) => Math.round(n * 100) / 100;
|
|
3178
3441
|
function glyphOutline(shape, cx, cy, half) {
|
|
3179
3442
|
if (shape === "square") {
|
|
3180
|
-
return `<rect x="${
|
|
3443
|
+
return `<rect x="${round9(cx - half)}" y="${round9(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS2}/>`;
|
|
3181
3444
|
}
|
|
3182
3445
|
if (shape === "circle") {
|
|
3183
3446
|
return `<circle cx="${cx}" cy="${cy}" r="${half}" ${GLYPH_ATTRS2}/>`;
|
|
3184
3447
|
}
|
|
3185
|
-
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}/>`;
|
|
3186
3449
|
}
|
|
3187
3450
|
function glyphVerticalExtentAt(shape, cy, half, dx) {
|
|
3188
3451
|
const ax = Math.min(Math.abs(dx), half);
|
|
@@ -3208,8 +3471,8 @@ function fillPartitions(n, half, inkByCondition) {
|
|
|
3208
3471
|
for (let s = 0; s <= SAMPLES; s++) {
|
|
3209
3472
|
const x = left + sliceW * s / SAMPLES;
|
|
3210
3473
|
const [yt, yb] = glyphVerticalExtentAt(n.shape, cy, half, x - cx);
|
|
3211
|
-
top.push(`${
|
|
3212
|
-
bottom.push(`${
|
|
3474
|
+
top.push(`${round9(x)},${round9(yt)}`);
|
|
3475
|
+
bottom.push(`${round9(x)},${round9(yb)}`);
|
|
3213
3476
|
}
|
|
3214
3477
|
const pts = [...top, ...bottom.reverse()].join(" ");
|
|
3215
3478
|
const ink = inkByCondition.get(id) ?? GLYPH_STROKE3;
|
|
@@ -3223,24 +3486,24 @@ function carrierDot(n) {
|
|
|
3223
3486
|
function deceasedSlash(n, half) {
|
|
3224
3487
|
if (!n.deceased) return "";
|
|
3225
3488
|
const ext = half + 4;
|
|
3226
|
-
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"/>`;
|
|
3227
3490
|
}
|
|
3228
3491
|
function probandArrow(n, half) {
|
|
3229
3492
|
if (n.role === null) return "";
|
|
3230
|
-
const tipX =
|
|
3231
|
-
const tipY =
|
|
3232
|
-
const tailX =
|
|
3233
|
-
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);
|
|
3234
3497
|
const filled = n.role === "proband";
|
|
3235
3498
|
const fill = filled ? GLYPH_STROKE3 : "transparent";
|
|
3236
3499
|
const shaft = `<line x1="${tailX}" y1="${tailY}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
|
|
3237
|
-
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"/>`;
|
|
3238
3501
|
return shaft + head;
|
|
3239
3502
|
}
|
|
3240
3503
|
function stillbirthMark(n, half) {
|
|
3241
3504
|
if (!n.stillbirth) return "";
|
|
3242
|
-
const y =
|
|
3243
|
-
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>`;
|
|
3244
3507
|
}
|
|
3245
3508
|
function nodeSvg2(n, inkByCondition) {
|
|
3246
3509
|
const half = n.size / 2;
|
|
@@ -3254,13 +3517,13 @@ function nodeSvg2(n, inkByCondition) {
|
|
|
3254
3517
|
probandArrow(n, half),
|
|
3255
3518
|
stillbirthMark(n, half)
|
|
3256
3519
|
];
|
|
3257
|
-
const addressY =
|
|
3520
|
+
const addressY = round9(n.labelTop + PED_ADDRESS_FONT);
|
|
3258
3521
|
pieces.push(
|
|
3259
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>`
|
|
3260
3523
|
);
|
|
3261
3524
|
if (n.labelLines.length > 0) {
|
|
3262
|
-
const firstBaseline =
|
|
3263
|
-
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("");
|
|
3264
3527
|
pieces.push(
|
|
3265
3528
|
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" fill="${LABEL_FILL4}">${tspans}</text>`
|
|
3266
3529
|
);
|
|
@@ -3275,7 +3538,7 @@ function elementSvg3(el) {
|
|
|
3275
3538
|
const title = `<title>${xmlEscape(el.title)}</title>`;
|
|
3276
3539
|
const stroke = `stroke="${EDGE_INK5}" stroke-width="1.5" stroke-opacity="0.75"`;
|
|
3277
3540
|
const draw = (offsetY) => {
|
|
3278
|
-
const shifted = pts.map((p) => ({ x: p.x, y:
|
|
3541
|
+
const shifted = pts.map((p) => ({ x: p.x, y: round9(p.y + offsetY) }));
|
|
3279
3542
|
if (shifted.length === 2) {
|
|
3280
3543
|
return `<line x1="${shifted[0].x}" y1="${shifted[0].y}" x2="${shifted[1].x}" y2="${shifted[1].y}" ${stroke}/>`;
|
|
3281
3544
|
}
|
|
@@ -3286,51 +3549,51 @@ function elementSvg3(el) {
|
|
|
3286
3549
|
}
|
|
3287
3550
|
function unknownTwinMarks(layout) {
|
|
3288
3551
|
return layout.unknownTwinJunctions.map(
|
|
3289
|
-
(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>`
|
|
3290
3553
|
).join("");
|
|
3291
3554
|
}
|
|
3292
3555
|
var MINI_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="1.5"`;
|
|
3293
3556
|
function miniShapeSwatch(shape, x, y) {
|
|
3294
|
-
const cx =
|
|
3295
|
-
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}/>`;
|
|
3296
3559
|
if (shape === "circle") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/>`;
|
|
3297
|
-
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}/>`;
|
|
3298
3561
|
}
|
|
3299
3562
|
function miniSwatchCircle(filled, ink, x, y) {
|
|
3300
|
-
const cx =
|
|
3563
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3301
3564
|
return `<circle cx="${cx}" cy="${y}" r="6" fill="${filled ? ink : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3302
3565
|
}
|
|
3303
3566
|
function miniCarrierSwatch(x, y) {
|
|
3304
|
-
const cx =
|
|
3567
|
+
const cx = round9(x + LEGEND_SWATCH_W / 2);
|
|
3305
3568
|
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><circle cx="${cx}" cy="${y}" r="2" fill="${GLYPH_STROKE3}" stroke="none"/>`;
|
|
3306
3569
|
}
|
|
3307
3570
|
function miniDeceasedSwatch(x, y) {
|
|
3308
|
-
const cx =
|
|
3309
|
-
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"/>`;
|
|
3310
3573
|
}
|
|
3311
3574
|
function miniArrowSwatch(filled, x, y) {
|
|
3312
|
-
const cx =
|
|
3313
|
-
const tipX =
|
|
3314
|
-
const tipY =
|
|
3315
|
-
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"/>`;
|
|
3316
3579
|
}
|
|
3317
3580
|
function miniConsanguineousSwatch(x, y) {
|
|
3318
|
-
const x1 =
|
|
3319
|
-
const x2 =
|
|
3320
|
-
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"/>`;
|
|
3321
3584
|
}
|
|
3322
3585
|
function miniTwinSwatch(zygosity, x, y) {
|
|
3323
|
-
const cx =
|
|
3324
|
-
const apexY =
|
|
3325
|
-
const baseY =
|
|
3326
|
-
const left =
|
|
3327
|
-
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);
|
|
3328
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"/>`;
|
|
3329
3592
|
if (zygosity === "mz") {
|
|
3330
|
-
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"/>`;
|
|
3331
3594
|
}
|
|
3332
3595
|
if (zygosity === "unknown") {
|
|
3333
|
-
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>`;
|
|
3334
3597
|
}
|
|
3335
3598
|
return stub;
|
|
3336
3599
|
}
|
|
@@ -3343,7 +3606,7 @@ function pedigreeLayoutSvg(layout, opts = {}) {
|
|
|
3343
3606
|
for (const n of layout.nodes) parts.push(nodeSvg2(n, inkByCondition));
|
|
3344
3607
|
for (const g of layout.generations) {
|
|
3345
3608
|
parts.push(
|
|
3346
|
-
`<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>`
|
|
3347
3610
|
);
|
|
3348
3611
|
}
|
|
3349
3612
|
let width = layout.width;
|
|
@@ -3509,13 +3772,13 @@ function phyloIssues(input) {
|
|
|
3509
3772
|
push("tip-with-children", `node ${n.id} is declared internal but has no children`);
|
|
3510
3773
|
}
|
|
3511
3774
|
}
|
|
3512
|
-
const
|
|
3775
|
+
const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
|
|
3513
3776
|
"duplicate-id",
|
|
3514
3777
|
"unknown-endpoint",
|
|
3515
3778
|
"unknown-root",
|
|
3516
3779
|
"multiple-parents"
|
|
3517
3780
|
]);
|
|
3518
|
-
if (!issues.some((i) =>
|
|
3781
|
+
if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
|
|
3519
3782
|
const childrenOf = /* @__PURE__ */ new Map();
|
|
3520
3783
|
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
3521
3784
|
const arr = childrenOf.get(e.parentId) ?? [];
|
|
@@ -3593,7 +3856,7 @@ var PHYLO_BRANCH_ID_BASE = 2e6;
|
|
|
3593
3856
|
var PHYLO_EXTENSION_ID_BASE = 3e6;
|
|
3594
3857
|
var PHYLO_ROOTSTUB_ID_BASE = 4e6;
|
|
3595
3858
|
var PHYLO_SCALEBAR_ID = 5e6;
|
|
3596
|
-
var
|
|
3859
|
+
var round10 = (n) => Math.round(n * 100) / 100;
|
|
3597
3860
|
function niceScaleStep(maxLen) {
|
|
3598
3861
|
if (!(maxLen > 0)) return 0;
|
|
3599
3862
|
const target = maxLen / 4;
|
|
@@ -3719,8 +3982,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3719
3982
|
edgeId: PHYLO_ROOTSTUB_ID_BASE + root.node.id,
|
|
3720
3983
|
kind: "root-stub",
|
|
3721
3984
|
points: [
|
|
3722
|
-
{ x:
|
|
3723
|
-
{ x:
|
|
3985
|
+
{ x: round10(Math.max(PADDING6 / 2, rootDrawX - ROOT_STUB)), y: round10(root.cy) },
|
|
3986
|
+
{ x: round10(rootDrawX), y: round10(root.cy) }
|
|
3724
3987
|
],
|
|
3725
3988
|
dotted: false,
|
|
3726
3989
|
length: null,
|
|
@@ -3735,11 +3998,11 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3735
3998
|
nodes.push({
|
|
3736
3999
|
nodeId: w.node.id,
|
|
3737
4000
|
isTip: tip,
|
|
3738
|
-
cx:
|
|
3739
|
-
cy:
|
|
4001
|
+
cx: round10(drawX),
|
|
4002
|
+
cy: round10(w.cy),
|
|
3740
4003
|
support,
|
|
3741
4004
|
labelLines: labelLine,
|
|
3742
|
-
labelLeft:
|
|
4005
|
+
labelLeft: round10(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
|
|
3743
4006
|
depth: w.depth,
|
|
3744
4007
|
title: nodeTitle(w.node, tip, isRoot, w.inLength, titleLabels)
|
|
3745
4008
|
});
|
|
@@ -3748,8 +4011,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3748
4011
|
edgeId: PHYLO_EXTENSION_ID_BASE + w.node.id,
|
|
3749
4012
|
kind: "extension",
|
|
3750
4013
|
points: [
|
|
3751
|
-
{ x:
|
|
3752
|
-
{ x:
|
|
4014
|
+
{ x: round10(w.cx), y: round10(w.cy) },
|
|
4015
|
+
{ x: round10(alignX), y: round10(w.cy) }
|
|
3753
4016
|
],
|
|
3754
4017
|
dotted: true,
|
|
3755
4018
|
length: null,
|
|
@@ -3765,8 +4028,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3765
4028
|
edgeId: PHYLO_CLADEBAR_ID_BASE + w.node.id,
|
|
3766
4029
|
kind: "clade-bar",
|
|
3767
4030
|
points: [
|
|
3768
|
-
{ x:
|
|
3769
|
-
{ x:
|
|
4031
|
+
{ x: round10(w.cx), y: round10(barTop) },
|
|
4032
|
+
{ x: round10(w.cx), y: round10(barBottom) }
|
|
3770
4033
|
],
|
|
3771
4034
|
dotted: false,
|
|
3772
4035
|
length: null,
|
|
@@ -3778,8 +4041,8 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3778
4041
|
edgeId: PHYLO_BRANCH_ID_BASE + c.node.id,
|
|
3779
4042
|
kind: "branch",
|
|
3780
4043
|
points: [
|
|
3781
|
-
{ x:
|
|
3782
|
-
{ x:
|
|
4044
|
+
{ x: round10(w.cx), y: round10(c.cy) },
|
|
4045
|
+
{ x: round10(c.cx), y: round10(c.cy) }
|
|
3783
4046
|
],
|
|
3784
4047
|
dotted: false,
|
|
3785
4048
|
length: c.inLength,
|
|
@@ -3796,17 +4059,17 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3796
4059
|
const barY = treeBottom + SCALEBAR_ZONE / 2;
|
|
3797
4060
|
scaleBar = {
|
|
3798
4061
|
length: stepLen,
|
|
3799
|
-
x:
|
|
3800
|
-
y:
|
|
3801
|
-
pxLength:
|
|
4062
|
+
x: round10(barX),
|
|
4063
|
+
y: round10(barY),
|
|
4064
|
+
pxLength: round10(scaleBarPx),
|
|
3802
4065
|
valueLabel: trimNumber(stepLen)
|
|
3803
4066
|
};
|
|
3804
4067
|
elements.push({
|
|
3805
4068
|
edgeId: PHYLO_SCALEBAR_ID,
|
|
3806
4069
|
kind: "scale-bar",
|
|
3807
4070
|
points: [
|
|
3808
|
-
{ x:
|
|
3809
|
-
{ x:
|
|
4071
|
+
{ x: round10(barX), y: round10(barY) },
|
|
4072
|
+
{ x: round10(barX + scaleBarPx), y: round10(barY) }
|
|
3810
4073
|
],
|
|
3811
4074
|
dotted: false,
|
|
3812
4075
|
length: stepLen,
|
|
@@ -3820,18 +4083,18 @@ function computePhyloLayout(input, opts = {}) {
|
|
|
3820
4083
|
var GLYPH_STROKE4 = "#52525b";
|
|
3821
4084
|
var LABEL_FILL5 = "#3f3f46";
|
|
3822
4085
|
var EDGE_INK6 = "#71717a";
|
|
3823
|
-
var
|
|
4086
|
+
var round11 = (n) => Math.round(n * 100) / 100;
|
|
3824
4087
|
function nodeSvg3(n, showSupport) {
|
|
3825
4088
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
3826
4089
|
pieces.push(`<circle cx="${n.cx}" cy="${n.cy}" r="2.5" fill="${GLYPH_STROKE4}"/>`);
|
|
3827
4090
|
if (n.labelLines.length > 0) {
|
|
3828
4091
|
pieces.push(
|
|
3829
|
-
`<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>`
|
|
3830
4093
|
);
|
|
3831
4094
|
}
|
|
3832
4095
|
if (showSupport && !n.isTip && n.support !== null) {
|
|
3833
4096
|
pieces.push(
|
|
3834
|
-
`<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>`
|
|
3835
4098
|
);
|
|
3836
4099
|
}
|
|
3837
4100
|
return `<g data-node-id="n${n.nodeId}">${pieces.join("")}</g>`;
|
|
@@ -3846,18 +4109,18 @@ function elementSvg4(el) {
|
|
|
3846
4109
|
function scaleBarLabelSvg(layout) {
|
|
3847
4110
|
const bar = layout.scaleBar;
|
|
3848
4111
|
if (bar === null) return "";
|
|
3849
|
-
const tick = (x) => `<line x1="${x}" y1="${
|
|
3850
|
-
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>`;
|
|
3851
4114
|
}
|
|
3852
4115
|
function supportSwatch(x, y) {
|
|
3853
|
-
const cx =
|
|
3854
|
-
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>`;
|
|
3855
4118
|
}
|
|
3856
4119
|
function scaleSwatch(x, y) {
|
|
3857
|
-
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"/>`;
|
|
3858
4121
|
}
|
|
3859
4122
|
function alignedTipSwatch(x, y) {
|
|
3860
|
-
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"/>`;
|
|
3861
4124
|
}
|
|
3862
4125
|
function phyloLayoutSvg(layout, opts = {}) {
|
|
3863
4126
|
const labels = opts.labels ?? PHYLO_SVG_LABELS_EN;
|
|
@@ -3962,9 +4225,9 @@ function orgChartIssues(input) {
|
|
|
3962
4225
|
}
|
|
3963
4226
|
const reportById = /* @__PURE__ */ new Map();
|
|
3964
4227
|
const dupReportIds = /* @__PURE__ */ new Set();
|
|
3965
|
-
for (const
|
|
3966
|
-
if (reportById.has(
|
|
3967
|
-
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);
|
|
3968
4231
|
}
|
|
3969
4232
|
for (const id of [...dupReportIds].sort((a, b) => a - b)) {
|
|
3970
4233
|
push("duplicate-id", `duplicate report id ${id}`);
|
|
@@ -3977,32 +4240,32 @@ function orgChartIssues(input) {
|
|
|
3977
4240
|
const assistantReportEdge = /* @__PURE__ */ new Map();
|
|
3978
4241
|
const solidManagedEdges = /* @__PURE__ */ new Map();
|
|
3979
4242
|
const dottedDegree = /* @__PURE__ */ new Map();
|
|
3980
|
-
for (const
|
|
3981
|
-
const hasManager = positionById.has(
|
|
3982
|
-
const hasReport = positionById.has(
|
|
4243
|
+
for (const r2 of reports) {
|
|
4244
|
+
const hasManager = positionById.has(r2.managerId);
|
|
4245
|
+
const hasReport = positionById.has(r2.reportId);
|
|
3983
4246
|
if (!hasManager) {
|
|
3984
|
-
push("unknown-manager", `report ${
|
|
4247
|
+
push("unknown-manager", `report ${r2.id} managerId ${r2.managerId} is not a declared position`);
|
|
3985
4248
|
}
|
|
3986
4249
|
if (!hasReport) {
|
|
3987
|
-
push("unknown-report", `report ${
|
|
4250
|
+
push("unknown-report", `report ${r2.id} reportId ${r2.reportId} is not a declared position`);
|
|
3988
4251
|
}
|
|
3989
|
-
if (
|
|
3990
|
-
push("self-report", `report ${
|
|
4252
|
+
if (r2.managerId === r2.reportId) {
|
|
4253
|
+
push("self-report", `report ${r2.id} has managerId === reportId (${r2.managerId})`);
|
|
3991
4254
|
}
|
|
3992
|
-
if (
|
|
3993
|
-
const arr = solidParents.get(
|
|
3994
|
-
arr.push(
|
|
3995
|
-
solidParents.set(
|
|
3996
|
-
const managed = solidManagedEdges.get(
|
|
3997
|
-
managed.push(
|
|
3998
|
-
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);
|
|
3999
4262
|
}
|
|
4000
|
-
if (
|
|
4001
|
-
assistantReportEdge.set(
|
|
4263
|
+
if (r2.kind === "assistant" && r2.managerId !== r2.reportId && !assistantReportEdge.has(r2.reportId)) {
|
|
4264
|
+
assistantReportEdge.set(r2.reportId, r2.id);
|
|
4002
4265
|
}
|
|
4003
|
-
if (
|
|
4004
|
-
if (positionById.has(
|
|
4005
|
-
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);
|
|
4006
4269
|
}
|
|
4007
4270
|
}
|
|
4008
4271
|
for (const [reportId, edgeIds] of [...solidParents.entries()].sort((a, b) => a[0] - b[0])) {
|
|
@@ -4033,12 +4296,12 @@ function orgChartIssues(input) {
|
|
|
4033
4296
|
}
|
|
4034
4297
|
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
4035
4298
|
const solidChildren = /* @__PURE__ */ new Map();
|
|
4036
|
-
for (const
|
|
4037
|
-
if (
|
|
4038
|
-
if (
|
|
4039
|
-
const arr = solidChildren.get(
|
|
4040
|
-
arr.push(
|
|
4041
|
-
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);
|
|
4042
4305
|
}
|
|
4043
4306
|
for (const arr of solidChildren.values()) arr.sort((a, b) => a - b);
|
|
4044
4307
|
const color = /* @__PURE__ */ new Map();
|
|
@@ -4105,26 +4368,9 @@ var ORG_BUS_ID_BASE = 2e6;
|
|
|
4105
4368
|
var ORG_DROP_ID_BASE = 3e6;
|
|
4106
4369
|
var ORG_ASSIST_ID_BASE = 4e6;
|
|
4107
4370
|
var ORG_DOTTED_ID_BASE = 5e6;
|
|
4108
|
-
var
|
|
4109
|
-
var drawnLeftEdge = (cx, boxW) =>
|
|
4110
|
-
var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) +
|
|
4111
|
-
function packSubtree(node, gaps) {
|
|
4112
|
-
const { ownHalfL, ownHalfR, children } = node;
|
|
4113
|
-
if (children.length === 0) {
|
|
4114
|
-
return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
|
|
4115
|
-
}
|
|
4116
|
-
const xs = [0];
|
|
4117
|
-
for (let i = 1; i < children.length; i++) {
|
|
4118
|
-
xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
|
|
4119
|
-
}
|
|
4120
|
-
const first = children[0];
|
|
4121
|
-
const last = children[children.length - 1];
|
|
4122
|
-
const axis = (xs[0] + xs[xs.length - 1]) / 2;
|
|
4123
|
-
const offsets = xs.map((x) => x - axis);
|
|
4124
|
-
const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
|
|
4125
|
-
const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
|
|
4126
|
-
return { halfL, halfR, offsets };
|
|
4127
|
-
}
|
|
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);
|
|
4128
4374
|
function measurePosition(position, maxLabelChars, vacantWord) {
|
|
4129
4375
|
const nameLines = position.name === "" ? [] : wrapLabelBalanced(clampLabel(position.name, maxLabelChars), 2);
|
|
4130
4376
|
const titleLines = position.title === null ? [] : wrapLabelBalanced(clampLabel(position.title, maxLabelChars), 2);
|
|
@@ -4157,16 +4403,16 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4157
4403
|
const vacantWord = titleLabels.vacant;
|
|
4158
4404
|
const positionById = new Map(input.positions.map((p) => [p.id, p]));
|
|
4159
4405
|
const reports = [...input.reports].sort((a, b) => a.id - b.id);
|
|
4160
|
-
const solidReports = reports.filter((
|
|
4406
|
+
const solidReports = reports.filter((r2) => r2.kind !== "dotted");
|
|
4161
4407
|
const lineChildIds = /* @__PURE__ */ new Map();
|
|
4162
4408
|
const assistChildIds = /* @__PURE__ */ new Map();
|
|
4163
4409
|
const solidParentReportId = /* @__PURE__ */ new Set();
|
|
4164
|
-
for (const
|
|
4165
|
-
solidParentReportId.add(
|
|
4166
|
-
const bucket =
|
|
4167
|
-
const arr = bucket.get(
|
|
4168
|
-
arr.push(
|
|
4169
|
-
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);
|
|
4170
4416
|
}
|
|
4171
4417
|
for (const arr of lineChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
4172
4418
|
for (const arr of assistChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
@@ -4196,12 +4442,12 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4196
4442
|
const position = positionById.get(positionId);
|
|
4197
4443
|
const inst = newInst(position, depth, assistReport);
|
|
4198
4444
|
allInsts.push(inst);
|
|
4199
|
-
for (const
|
|
4200
|
-
const a = build(
|
|
4201
|
-
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 });
|
|
4202
4448
|
}
|
|
4203
|
-
for (const
|
|
4204
|
-
inst.lineChildren.push(build(
|
|
4449
|
+
for (const r2 of lineChildIds.get(positionId) ?? []) {
|
|
4450
|
+
inst.lineChildren.push(build(r2.reportId, depth + 1, null));
|
|
4205
4451
|
}
|
|
4206
4452
|
return inst;
|
|
4207
4453
|
};
|
|
@@ -4275,10 +4521,10 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4275
4521
|
const pushNode = (inst) => {
|
|
4276
4522
|
nodes.push({
|
|
4277
4523
|
positionId: inst.position.id,
|
|
4278
|
-
cx:
|
|
4279
|
-
top:
|
|
4280
|
-
boxW:
|
|
4281
|
-
boxH:
|
|
4524
|
+
cx: round12(inst.cx),
|
|
4525
|
+
top: round12(rowTop[inst.depth]),
|
|
4526
|
+
boxW: round12(inst.m.boxW),
|
|
4527
|
+
boxH: round12(inst.m.boxH),
|
|
4282
4528
|
style: inst.style,
|
|
4283
4529
|
nameLines: inst.m.nameLines,
|
|
4284
4530
|
titleLines: inst.m.titleLines,
|
|
@@ -4286,7 +4532,8 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4286
4532
|
vacantMarker: inst.m.vacantMarker,
|
|
4287
4533
|
isAssistant: inst.assistReport !== null,
|
|
4288
4534
|
depth: inst.depth,
|
|
4289
|
-
title: inst.title
|
|
4535
|
+
title: inst.title,
|
|
4536
|
+
annotated: inst.position.annotated ?? false
|
|
4290
4537
|
});
|
|
4291
4538
|
};
|
|
4292
4539
|
const emit = (inst) => {
|
|
@@ -4300,10 +4547,10 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4300
4547
|
const assistMidY = assistTop + a.m.boxH / 2;
|
|
4301
4548
|
nodes.push({
|
|
4302
4549
|
positionId: a.position.id,
|
|
4303
|
-
cx:
|
|
4304
|
-
top:
|
|
4305
|
-
boxW:
|
|
4306
|
-
boxH:
|
|
4550
|
+
cx: round12(a.cx),
|
|
4551
|
+
top: round12(assistTop),
|
|
4552
|
+
boxW: round12(a.m.boxW),
|
|
4553
|
+
boxH: round12(a.m.boxH),
|
|
4307
4554
|
style: a.style,
|
|
4308
4555
|
nameLines: a.m.nameLines,
|
|
4309
4556
|
titleLines: a.m.titleLines,
|
|
@@ -4311,17 +4558,19 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4311
4558
|
vacantMarker: a.m.vacantMarker,
|
|
4312
4559
|
isAssistant: true,
|
|
4313
4560
|
depth: a.depth,
|
|
4314
|
-
title: a.title
|
|
4561
|
+
title: a.title,
|
|
4562
|
+
annotated: a.position.annotated ?? false
|
|
4315
4563
|
});
|
|
4316
4564
|
elements.push({
|
|
4317
4565
|
edgeId: ORG_ASSIST_ID_BASE + report.reportId,
|
|
4318
4566
|
kind: "assist",
|
|
4319
4567
|
points: [
|
|
4320
|
-
{ x:
|
|
4321
|
-
{ 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) }
|
|
4322
4570
|
],
|
|
4323
4571
|
dashed: false,
|
|
4324
|
-
title: reportTitle("assistant", report.label)
|
|
4572
|
+
title: reportTitle("assistant", report.label),
|
|
4573
|
+
annotated: report.annotated ?? false
|
|
4325
4574
|
});
|
|
4326
4575
|
bandY += a.m.boxH + ORG_ASSIST_BAND_DROP;
|
|
4327
4576
|
}
|
|
@@ -4333,11 +4582,12 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4333
4582
|
edgeId: ORG_STEM_ID_BASE + inst.position.id,
|
|
4334
4583
|
kind: "stem",
|
|
4335
4584
|
points: [
|
|
4336
|
-
{ x:
|
|
4337
|
-
{ x:
|
|
4585
|
+
{ x: round12(inst.cx), y: round12(boxBottom) },
|
|
4586
|
+
{ x: round12(inst.cx), y: round12(by) }
|
|
4338
4587
|
],
|
|
4339
4588
|
dashed: false,
|
|
4340
|
-
title: reportTitle("line", null)
|
|
4589
|
+
title: reportTitle("line", null),
|
|
4590
|
+
annotated: false
|
|
4341
4591
|
});
|
|
4342
4592
|
const childCxs = inst.lineChildren.map((c) => c.cx);
|
|
4343
4593
|
if (inst.lineChildren.length > 1) {
|
|
@@ -4345,23 +4595,28 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4345
4595
|
edgeId: ORG_BUS_ID_BASE + inst.position.id,
|
|
4346
4596
|
kind: "bus",
|
|
4347
4597
|
points: [
|
|
4348
|
-
{ x:
|
|
4349
|
-
{ x:
|
|
4598
|
+
{ x: round12(Math.min(...childCxs)), y: round12(by) },
|
|
4599
|
+
{ x: round12(Math.max(...childCxs)), y: round12(by) }
|
|
4350
4600
|
],
|
|
4351
4601
|
dashed: false,
|
|
4352
|
-
title: reportTitle("line", null)
|
|
4602
|
+
title: reportTitle("line", null),
|
|
4603
|
+
annotated: false
|
|
4353
4604
|
});
|
|
4354
4605
|
}
|
|
4606
|
+
const lineReports = lineChildIds.get(inst.position.id) ?? [];
|
|
4607
|
+
const lineReportByChildId = new Map(lineReports.map((r2) => [r2.reportId, r2]));
|
|
4355
4608
|
for (const c of inst.lineChildren) {
|
|
4609
|
+
const lineReport = lineReportByChildId.get(c.position.id);
|
|
4356
4610
|
elements.push({
|
|
4357
4611
|
edgeId: ORG_DROP_ID_BASE + c.position.id,
|
|
4358
4612
|
kind: "drop",
|
|
4359
4613
|
points: [
|
|
4360
|
-
{ x:
|
|
4361
|
-
{ x:
|
|
4614
|
+
{ x: round12(c.cx), y: round12(by) },
|
|
4615
|
+
{ x: round12(c.cx), y: round12(rowTop[c.depth]) }
|
|
4362
4616
|
],
|
|
4363
4617
|
dashed: false,
|
|
4364
|
-
title: reportTitle("line", null)
|
|
4618
|
+
title: reportTitle("line", null),
|
|
4619
|
+
annotated: lineReport?.annotated ?? false
|
|
4365
4620
|
});
|
|
4366
4621
|
}
|
|
4367
4622
|
for (const c of inst.lineChildren) emit(c);
|
|
@@ -4395,7 +4650,7 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4395
4650
|
}
|
|
4396
4651
|
const nodeById = new Map(nodes.map((n) => [n.positionId, n]));
|
|
4397
4652
|
const dottedReports = reports.filter(
|
|
4398
|
-
(
|
|
4653
|
+
(r2) => r2.kind === "dotted" && nodeById.has(r2.managerId) && nodeById.has(r2.reportId) && r2.managerId !== r2.reportId
|
|
4399
4654
|
);
|
|
4400
4655
|
let maxBoxBottom = PADDING7;
|
|
4401
4656
|
for (const n of nodes) maxBoxBottom = Math.max(maxBoxBottom, n.top + n.boxH);
|
|
@@ -4403,14 +4658,14 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4403
4658
|
let channelLanes = 0;
|
|
4404
4659
|
if (dottedReports.length > 0) {
|
|
4405
4660
|
const laneOf = /* @__PURE__ */ new Map();
|
|
4406
|
-
laneCount =
|
|
4407
|
-
dottedReports.map((
|
|
4408
|
-
const rep = escapeByPos.get(
|
|
4409
|
-
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);
|
|
4410
4665
|
return {
|
|
4411
4666
|
lo: Math.min(rep.midY, mgr.midY),
|
|
4412
4667
|
hi: Math.max(rep.midY, mgr.midY),
|
|
4413
|
-
set: (lane) => laneOf.set(
|
|
4668
|
+
set: (lane) => laneOf.set(r2.id, lane)
|
|
4414
4669
|
};
|
|
4415
4670
|
})
|
|
4416
4671
|
);
|
|
@@ -4425,19 +4680,19 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4425
4680
|
const sign = slot % 2 === 1 ? -1 : 1;
|
|
4426
4681
|
return mid + sign * Math.ceil(slot / 2) * step;
|
|
4427
4682
|
};
|
|
4428
|
-
const geoms = dottedReports.map((
|
|
4429
|
-
const rep = escapeByPos.get(
|
|
4430
|
-
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);
|
|
4431
4686
|
return {
|
|
4432
|
-
r,
|
|
4687
|
+
r: r2,
|
|
4433
4688
|
i,
|
|
4434
|
-
repMidY:
|
|
4435
|
-
mgrMidY:
|
|
4436
|
-
repBoxEdge:
|
|
4437
|
-
mgrBoxEdge:
|
|
4438
|
-
repChY:
|
|
4439
|
-
mgrChY:
|
|
4440
|
-
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),
|
|
4441
4696
|
repEscX: 0,
|
|
4442
4697
|
mgrEscX: 0
|
|
4443
4698
|
};
|
|
@@ -4475,14 +4730,14 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4475
4730
|
else if (v.side === -1 && bRight < v.escCol - VERT_EPS) band = Math.min(band, v.escCol - bRight);
|
|
4476
4731
|
}
|
|
4477
4732
|
let lane = 0;
|
|
4478
|
-
let candidate =
|
|
4733
|
+
let candidate = round12(v.escCol);
|
|
4479
4734
|
while (placed.some((p) => Math.abs(p.x - candidate) < VERT_EPS && yOverlap(p.lo, p.hi, v.lo, v.hi))) {
|
|
4480
4735
|
lane += 1;
|
|
4481
|
-
candidate =
|
|
4736
|
+
candidate = round12(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
|
|
4482
4737
|
}
|
|
4483
4738
|
if (lane > 0 && lane * ORG_MATRIX_ESCAPE_STEP >= band - VERT_EPS) {
|
|
4484
4739
|
throw new Error(
|
|
4485
|
-
`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`
|
|
4486
4741
|
);
|
|
4487
4742
|
}
|
|
4488
4743
|
placed.push({ x: candidate, lo: v.lo, hi: v.hi });
|
|
@@ -4511,7 +4766,8 @@ function computeOrgChartLayout(input, opts = {}) {
|
|
|
4511
4766
|
// → into the manager box's edge
|
|
4512
4767
|
]),
|
|
4513
4768
|
dashed: true,
|
|
4514
|
-
title: reportTitle("dotted", g.r.label)
|
|
4769
|
+
title: reportTitle("dotted", g.r.label),
|
|
4770
|
+
annotated: g.r.annotated ?? false
|
|
4515
4771
|
});
|
|
4516
4772
|
}
|
|
4517
4773
|
}
|
|
@@ -4527,25 +4783,6 @@ function collapseDegenerate(points) {
|
|
|
4527
4783
|
}
|
|
4528
4784
|
return out;
|
|
4529
4785
|
}
|
|
4530
|
-
function allocateLanes2(items) {
|
|
4531
|
-
const lanes = [];
|
|
4532
|
-
for (const it of items) {
|
|
4533
|
-
let chosen = -1;
|
|
4534
|
-
for (let l = 0; l < lanes.length; l++) {
|
|
4535
|
-
if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
|
|
4536
|
-
chosen = l;
|
|
4537
|
-
break;
|
|
4538
|
-
}
|
|
4539
|
-
}
|
|
4540
|
-
if (chosen === -1) {
|
|
4541
|
-
chosen = lanes.length;
|
|
4542
|
-
lanes.push([]);
|
|
4543
|
-
}
|
|
4544
|
-
lanes[chosen].push({ lo: it.lo, hi: it.hi });
|
|
4545
|
-
it.set(chosen);
|
|
4546
|
-
}
|
|
4547
|
-
return lanes.length;
|
|
4548
|
-
}
|
|
4549
4786
|
|
|
4550
4787
|
// src/org-chart/svg.ts
|
|
4551
4788
|
var GLYPH_STROKE5 = "#52525b";
|
|
@@ -4554,25 +4791,25 @@ var EDGE_INK7 = "#71717a";
|
|
|
4554
4791
|
var GLYPH_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="2"`;
|
|
4555
4792
|
var VACANT_DASH = `stroke-dasharray="6,4"`;
|
|
4556
4793
|
var DOTTED = EDGE_STROKE.distant;
|
|
4557
|
-
var
|
|
4794
|
+
var round13 = (n) => Math.round(n * 100) / 100;
|
|
4558
4795
|
var ORG_BOX_PAD_Y2 = 9;
|
|
4559
4796
|
function boxSvg(n) {
|
|
4560
|
-
const left =
|
|
4797
|
+
const left = round13(n.cx - n.boxW / 2);
|
|
4561
4798
|
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
4562
4799
|
if (n.style === "dashed") {
|
|
4563
4800
|
pieces.push(
|
|
4564
4801
|
`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`
|
|
4565
4802
|
);
|
|
4566
|
-
const ix =
|
|
4567
|
-
const iy =
|
|
4568
|
-
const iw =
|
|
4569
|
-
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);
|
|
4570
4807
|
pieces.push(`<rect x="${ix}" y="${iy}" width="${iw}" height="${ih}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`);
|
|
4571
4808
|
} else {
|
|
4572
4809
|
pieces.push(`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3}/>`);
|
|
4573
4810
|
}
|
|
4574
4811
|
let lineIndex = 0;
|
|
4575
|
-
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);
|
|
4576
4813
|
const textBlock = (lines, font) => {
|
|
4577
4814
|
if (lines.length === 0) return "";
|
|
4578
4815
|
const tspans = lines.map((line, i) => `<tspan x="${n.cx}" y="${firstBaseline(lineIndex + i)}">${xmlEscape(line)}</tspan>`).join("");
|
|
@@ -4583,43 +4820,45 @@ function boxSvg(n) {
|
|
|
4583
4820
|
pieces.push(textBlock(n.titleLines, ORG_TITLE_FONT));
|
|
4584
4821
|
pieces.push(textBlock(n.subtitleLines, ORG_TITLE_FONT));
|
|
4585
4822
|
if (n.vacantMarker !== null) pieces.push(textBlock([n.vacantMarker], ORG_TITLE_FONT));
|
|
4823
|
+
if (n.annotated) pieces.push(annotationDot(round13(n.cx + n.boxW / 2) - 4, n.top + 4));
|
|
4586
4824
|
return `<g data-node-id="p${n.positionId}">${pieces.filter((p) => p !== "").join("")}</g>`;
|
|
4587
4825
|
}
|
|
4588
4826
|
function elementSvg5(el) {
|
|
4589
4827
|
const head = `<g data-edge-id="${el.edgeId}"><title>${xmlEscape(el.title)}</title>`;
|
|
4828
|
+
const tick = el.annotated ? annotationTick(el.points) : "";
|
|
4590
4829
|
if (el.dashed) {
|
|
4591
4830
|
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
4592
|
-
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}
|
|
4831
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>${tick}</g>`;
|
|
4593
4832
|
}
|
|
4594
4833
|
if (el.points.length === 2) {
|
|
4595
4834
|
const a = el.points[0];
|
|
4596
4835
|
const b = el.points[1];
|
|
4597
|
-
return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"
|
|
4836
|
+
return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>${tick}</g>`;
|
|
4598
4837
|
}
|
|
4599
|
-
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"
|
|
4838
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>${tick}</g>`;
|
|
4600
4839
|
}
|
|
4601
4840
|
var MINI_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="1.5"`;
|
|
4602
4841
|
function lineSwatch(x, y) {
|
|
4603
|
-
const x1 =
|
|
4604
|
-
const x2 =
|
|
4842
|
+
const x1 = round13(x + 2);
|
|
4843
|
+
const x2 = round13(x + LEGEND_SWATCH_W - 2);
|
|
4605
4844
|
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
4606
4845
|
}
|
|
4607
4846
|
function assistantSwatch(x, y) {
|
|
4608
|
-
const stemX =
|
|
4609
|
-
const boxX =
|
|
4610
|
-
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}/>`;
|
|
4611
4850
|
}
|
|
4612
4851
|
function dottedSwatch(x, y) {
|
|
4613
|
-
const x1 =
|
|
4614
|
-
const x2 =
|
|
4852
|
+
const x1 = round13(x + 2);
|
|
4853
|
+
const x2 = round13(x + LEGEND_SWATCH_W - 2);
|
|
4615
4854
|
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
4616
4855
|
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>`;
|
|
4617
4856
|
}
|
|
4618
4857
|
function vacantSwatch(x, y) {
|
|
4619
|
-
const cx =
|
|
4620
|
-
const bx =
|
|
4621
|
-
const by =
|
|
4622
|
-
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"/>`;
|
|
4623
4862
|
}
|
|
4624
4863
|
function orgChartLayoutSvg(layout, opts = {}) {
|
|
4625
4864
|
const labels = opts.labels ?? ORG_CHART_SVG_LABELS_EN;
|
|
@@ -4638,6 +4877,9 @@ function orgChartLayoutSvg(layout, opts = {}) {
|
|
|
4638
4877
|
if (hasAssistant) entries.push({ swatch: assistantSwatch, label: labels.legend.assistant });
|
|
4639
4878
|
if (hasDotted) entries.push({ swatch: dottedSwatch, label: labels.legend.dotted });
|
|
4640
4879
|
if (hasVacant) entries.push({ swatch: vacantSwatch, label: labels.legend.vacant });
|
|
4880
|
+
if (opts.annotationLabel !== void 0 && (layout.nodes.some((n) => n.annotated) || layout.elements.some((e) => e.annotated))) {
|
|
4881
|
+
entries.push({ swatch: annotationSwatch, label: opts.annotationLabel });
|
|
4882
|
+
}
|
|
4641
4883
|
const block = legendBlock(entries, layout.height);
|
|
4642
4884
|
if (block.svg !== "") {
|
|
4643
4885
|
parts.push(block.svg);
|
|
@@ -4658,11 +4900,1661 @@ function orgChartSvg(input, opts = {}) {
|
|
|
4658
4900
|
});
|
|
4659
4901
|
const svg = orgChartLayoutSvg(layout, {
|
|
4660
4902
|
...opts.legend === false ? { legend: false } : {},
|
|
4661
|
-
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
4903
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {},
|
|
4904
|
+
...opts.annotationLabel !== void 0 ? { annotationLabel: opts.annotationLabel } : {}
|
|
4662
4905
|
});
|
|
4663
4906
|
return { svg, layout };
|
|
4664
4907
|
}
|
|
4665
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
|
+
|
|
6557
|
+
exports.ANNOTATION_INK = ANNOTATION_INK;
|
|
4666
6558
|
exports.CHAR_W = CHAR_W;
|
|
4667
6559
|
exports.CODE_FONT = CODE_FONT;
|
|
4668
6560
|
exports.ECOMAP_LABELS_EN = ECOMAP_LABELS_EN;
|
|
@@ -4685,6 +6577,12 @@ exports.FishboneValidationError = FishboneValidationError;
|
|
|
4685
6577
|
exports.GATE_TYPES = GATE_TYPES;
|
|
4686
6578
|
exports.GENOGRAM_SVG_LABELS_EN = GENOGRAM_SVG_LABELS_EN;
|
|
4687
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;
|
|
4688
6586
|
exports.KINSHIP_EN = KINSHIP_EN;
|
|
4689
6587
|
exports.LABEL_FONT = LABEL_FONT;
|
|
4690
6588
|
exports.LABEL_GAP = LABEL_GAP;
|
|
@@ -4736,13 +6634,63 @@ exports.PHYLO_SCALEBAR_ID = PHYLO_SCALEBAR_ID;
|
|
|
4736
6634
|
exports.PHYLO_SUPPORT_FONT = PHYLO_SUPPORT_FONT;
|
|
4737
6635
|
exports.PHYLO_SVG_LABELS_EN = PHYLO_SVG_LABELS_EN;
|
|
4738
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;
|
|
4739
6656
|
exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
|
|
4740
6657
|
exports.PedigreeValidationError = PedigreeValidationError;
|
|
4741
6658
|
exports.PhyloValidationError = PhyloValidationError;
|
|
6659
|
+
exports.PrismaValidationError = PrismaValidationError;
|
|
4742
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;
|
|
4743
6683
|
exports.UNION_NOTATION = UNION_NOTATION;
|
|
4744
6684
|
exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
|
|
4745
6685
|
exports.UNION_STATUSES = UNION_STATUSES;
|
|
6686
|
+
exports.UmlValidationError = UmlValidationError;
|
|
6687
|
+
exports.allocateLanes = allocateLanes;
|
|
6688
|
+
exports.annotationDot = annotationDot;
|
|
6689
|
+
exports.annotationSwatch = annotationSwatch;
|
|
6690
|
+
exports.annotationTick = annotationTick;
|
|
6691
|
+
exports.arrowFilledPoints = arrowFilledPoints;
|
|
6692
|
+
exports.arrowOpenPoints = arrowOpenPoints;
|
|
6693
|
+
exports.bandStack = bandStack;
|
|
4746
6694
|
exports.clampLabel = clampLabel;
|
|
4747
6695
|
exports.classifyRelationshipType = classifyRelationshipType;
|
|
4748
6696
|
exports.computeFaultTreeLayout = computeFaultTreeLayout;
|
|
@@ -4750,6 +6698,9 @@ exports.computeGenogramLayout = computeGenogramLayout;
|
|
|
4750
6698
|
exports.computeOrgChartLayout = computeOrgChartLayout;
|
|
4751
6699
|
exports.computePedigreeLayout = computePedigreeLayout;
|
|
4752
6700
|
exports.computePhyloLayout = computePhyloLayout;
|
|
6701
|
+
exports.computePrismaLayout = computePrismaLayout;
|
|
6702
|
+
exports.computeUmlLayout = computeUmlLayout;
|
|
6703
|
+
exports.diamondPoints = diamondPoints;
|
|
4753
6704
|
exports.ecomapSvg = ecomapSvg;
|
|
4754
6705
|
exports.estimateTextWidth = estimateTextWidth;
|
|
4755
6706
|
exports.faultTreeIssues = faultTreeIssues;
|
|
@@ -4758,13 +6709,16 @@ exports.faultTreeSvg = faultTreeSvg;
|
|
|
4758
6709
|
exports.fishboneSvg = fishboneSvg;
|
|
4759
6710
|
exports.genogramLayoutSvg = genogramLayoutSvg;
|
|
4760
6711
|
exports.genogramSvg = genogramSvg;
|
|
6712
|
+
exports.glyphInset = glyphInset;
|
|
4761
6713
|
exports.latestUnionPerPair = latestUnionPerPair;
|
|
4762
6714
|
exports.legendBlock = legendBlock;
|
|
6715
|
+
exports.measureCompartmentBox = measureCompartmentBox;
|
|
4763
6716
|
exports.niceScaleStep = niceScaleStep;
|
|
4764
6717
|
exports.normalizeText = normalizeText;
|
|
4765
6718
|
exports.orgChartIssues = orgChartIssues;
|
|
4766
6719
|
exports.orgChartLayoutSvg = orgChartLayoutSvg;
|
|
4767
6720
|
exports.orgChartSvg = orgChartSvg;
|
|
6721
|
+
exports.packGrid = packGrid;
|
|
4768
6722
|
exports.packSubtree = packSubtree;
|
|
4769
6723
|
exports.pathData = pathData;
|
|
4770
6724
|
exports.pedigreeIssues = pedigreeIssues;
|
|
@@ -4773,13 +6727,24 @@ exports.pedigreeSvg = pedigreeSvg;
|
|
|
4773
6727
|
exports.phyloIssues = phyloIssues;
|
|
4774
6728
|
exports.phyloLayoutSvg = phyloLayoutSvg;
|
|
4775
6729
|
exports.phyloSvg = phyloSvg;
|
|
6730
|
+
exports.prismaIssues = prismaIssues;
|
|
6731
|
+
exports.prismaLayoutSvg = prismaLayoutSvg;
|
|
6732
|
+
exports.prismaSvg = prismaSvg;
|
|
4776
6733
|
exports.qualityLineStyle = qualityLineStyle;
|
|
4777
6734
|
exports.relationshipTypeTokens = relationshipTypeTokens;
|
|
4778
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;
|
|
4779
6742
|
exports.validateFaultTree = validateFaultTree;
|
|
4780
6743
|
exports.validateOrgChart = validateOrgChart;
|
|
4781
6744
|
exports.validatePedigree = validatePedigree;
|
|
4782
6745
|
exports.validatePhylo = validatePhylo;
|
|
6746
|
+
exports.validatePrisma = validatePrisma;
|
|
6747
|
+
exports.validateUml = validateUml;
|
|
4783
6748
|
exports.wrapLabel = wrapLabel;
|
|
4784
6749
|
exports.wrapLabelBalanced = wrapLabelBalanced;
|
|
4785
6750
|
exports.xmlEscape = xmlEscape;
|