compasso 0.1.0 → 0.2.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 +63 -8
- package/dist/{chunk-E456YKAJ.js → chunk-5B453C4P.js} +40 -10
- package/dist/chunk-5B453C4P.js.map +1 -0
- package/dist/{chunk-5RRRE2GF.js → chunk-5PGOL2KR.js} +3 -3
- package/dist/{chunk-5RRRE2GF.js.map → chunk-5PGOL2KR.js.map} +1 -1
- package/dist/{chunk-L5CYESBI.js → chunk-EHQMKVDM.js} +3 -3
- package/dist/{chunk-L5CYESBI.js.map → chunk-EHQMKVDM.js.map} +1 -1
- package/dist/chunk-P2S7AUOL.js +703 -0
- package/dist/chunk-P2S7AUOL.js.map +1 -0
- package/dist/chunk-TP3JOOJW.js +252 -0
- package/dist/chunk-TP3JOOJW.js.map +1 -0
- package/dist/core/index.cjs +44 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +30 -30
- package/dist/core/index.d.ts +30 -30
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +11 -7
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.cjs +782 -0
- package/dist/fault-tree/index.cjs.map +1 -0
- package/dist/fault-tree/index.d.cts +148 -0
- package/dist/fault-tree/index.d.ts +148 -0
- package/dist/fault-tree/index.js +4 -0
- package/dist/fault-tree/index.js.map +1 -0
- package/dist/fishbone/index.cjs +314 -0
- package/dist/fishbone/index.cjs.map +1 -0
- package/dist/fishbone/index.d.cts +91 -0
- package/dist/fishbone/index.d.ts +91 -0
- package/dist/fishbone/index.js +4 -0
- package/dist/fishbone/index.js.map +1 -0
- package/dist/genogram/index.cjs +11 -7
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +3 -2
- package/dist/genogram/index.d.ts +3 -2
- package/dist/genogram/index.js +2 -2
- package/dist/index.cjs +1040 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -3
- package/dist/labels-CYbM5XV7.d.cts +83 -0
- package/dist/labels-CYbM5XV7.d.ts +83 -0
- package/dist/locales/pt-br.cjs +36 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +6 -1
- package/dist/locales/pt-br.d.ts +6 -1
- package/dist/locales/pt-br.js +34 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/text-DuO_PwYw.d.cts +45 -0
- package/dist/text-DuO_PwYw.d.ts +45 -0
- package/dist/xml-DDae1eUr.d.cts +4 -0
- package/dist/xml-DDae1eUr.d.ts +4 -0
- package/package.json +70 -18
- package/dist/chunk-E456YKAJ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -15,19 +15,27 @@ var CHAR_W = 0.6;
|
|
|
15
15
|
function estimateTextWidth(text, fontPx) {
|
|
16
16
|
return text.length * fontPx * CHAR_W;
|
|
17
17
|
}
|
|
18
|
-
function wrapLabel(label, perLine) {
|
|
18
|
+
function wrapLabel(label, perLine, maxLines = 2) {
|
|
19
19
|
if (label.length <= perLine) return [label];
|
|
20
20
|
const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
|
|
21
|
-
|
|
22
|
-
let line2 = "";
|
|
21
|
+
const lines = [""];
|
|
23
22
|
for (const word of label.split(/\s+/)) {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const last = lines.length - 1;
|
|
24
|
+
const current = lines[last];
|
|
25
|
+
if (current === "" || (current + " " + word).length <= perLine) {
|
|
26
|
+
lines[last] = current === "" ? word : `${current} ${word}`;
|
|
27
|
+
} else if (lines.length < maxLines) {
|
|
28
|
+
lines.push(word);
|
|
26
29
|
} else {
|
|
27
|
-
|
|
30
|
+
lines[last] = `${current} ${word}`;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
34
|
+
return lines.map(cap);
|
|
35
|
+
}
|
|
36
|
+
function wrapLabelBalanced(label, maxLines) {
|
|
37
|
+
const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));
|
|
38
|
+
return wrapLabel(label, perLine, maxLines);
|
|
31
39
|
}
|
|
32
40
|
function clampLabel(label, maxChars) {
|
|
33
41
|
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
@@ -35,6 +43,28 @@ function clampLabel(label, maxChars) {
|
|
|
35
43
|
}
|
|
36
44
|
var FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
37
45
|
|
|
46
|
+
// src/core/legend.ts
|
|
47
|
+
var LEGEND_ROW_H = 18;
|
|
48
|
+
var LEGEND_PAD = 16;
|
|
49
|
+
var LEGEND_SWATCH_W = 22;
|
|
50
|
+
var LEGEND_GAP = 14;
|
|
51
|
+
var LEGEND_FONT = 11;
|
|
52
|
+
var LEGEND_TEXT_FILL = "#52525b";
|
|
53
|
+
function legendBlock(entries, startY) {
|
|
54
|
+
if (entries.length === 0) return { svg: "", width: 0, height: startY };
|
|
55
|
+
const rows = entries.map((entry, i) => {
|
|
56
|
+
const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
|
|
57
|
+
const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
|
|
58
|
+
return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${LEGEND_TEXT_FILL}">${xmlEscape(entry.label)}</text>`;
|
|
59
|
+
});
|
|
60
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
|
|
61
|
+
return {
|
|
62
|
+
svg: `<g data-compasso-legend="true">${rows.join("")}</g>`,
|
|
63
|
+
width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,
|
|
64
|
+
height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
// src/core/stroke.ts
|
|
39
69
|
var EDGE_STROKE = {
|
|
40
70
|
plain: { width: 1.5, dash: null, opacity: 0.6 },
|
|
@@ -1001,11 +1031,11 @@ var STRUCT_WIDTH = 1.5;
|
|
|
1001
1031
|
var STRUCT_OPACITY = 0.75;
|
|
1002
1032
|
var DOTTED_DASH = [2, 3];
|
|
1003
1033
|
var DOTTED_OPACITY = 0.55;
|
|
1004
|
-
var
|
|
1005
|
-
var
|
|
1006
|
-
var
|
|
1007
|
-
var
|
|
1008
|
-
var
|
|
1034
|
+
var LEGEND_ROW_H2 = 18;
|
|
1035
|
+
var LEGEND_PAD2 = 16;
|
|
1036
|
+
var LEGEND_SWATCH_W2 = 22;
|
|
1037
|
+
var LEGEND_GAP2 = 14;
|
|
1038
|
+
var LEGEND_FONT2 = 11;
|
|
1009
1039
|
function glyphSvg(shape, cx, cy, half) {
|
|
1010
1040
|
const stroke = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="2"`;
|
|
1011
1041
|
if (shape === "square") {
|
|
@@ -1115,13 +1145,13 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1115
1145
|
for (const shape of ["square", "circle", "diamond"]) {
|
|
1116
1146
|
if (!shapesUsed.has(shape)) continue;
|
|
1117
1147
|
entries.push({
|
|
1118
|
-
swatch: (x, y) => glyphSvg(shape, x +
|
|
1148
|
+
swatch: (x, y) => glyphSvg(shape, x + LEGEND_SWATCH_W2 / 2, y, 6),
|
|
1119
1149
|
label: labels.shapes[shape]
|
|
1120
1150
|
});
|
|
1121
1151
|
}
|
|
1122
1152
|
if (layout.nodes.some((n) => n.deceased)) {
|
|
1123
1153
|
entries.push({
|
|
1124
|
-
swatch: (x, y) => glyphSvg("square", x +
|
|
1154
|
+
swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W2 / 2, y, 6) + `<line x1="${x + LEGEND_SWATCH_W2 / 2 - 6}" y1="${y - 6}" x2="${x + LEGEND_SWATCH_W2 / 2 + 6}" y2="${y + 6}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`,
|
|
1125
1155
|
label: labels.deceased
|
|
1126
1156
|
});
|
|
1127
1157
|
}
|
|
@@ -1131,27 +1161,27 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1131
1161
|
const ink = EDGE_STROKE[style];
|
|
1132
1162
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1133
1163
|
entries.push({
|
|
1134
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1164
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W2}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
|
|
1135
1165
|
label: labels.bondStyles[style]
|
|
1136
1166
|
});
|
|
1137
1167
|
}
|
|
1138
1168
|
if (layout.isolatedPersonIds.length > 0) {
|
|
1139
1169
|
entries.push({
|
|
1140
|
-
swatch: (x, y) => glyphSvg("square", x +
|
|
1170
|
+
swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W2 / 2, y, 6),
|
|
1141
1171
|
label: labels.isolated
|
|
1142
1172
|
});
|
|
1143
1173
|
}
|
|
1144
1174
|
if (entries.length > 0) {
|
|
1145
1175
|
const startY = layout.height;
|
|
1146
1176
|
const rows = entries.map((entry, i) => {
|
|
1147
|
-
const rowCenterY = startY + i *
|
|
1148
|
-
const textX =
|
|
1149
|
-
return entry.swatch(
|
|
1177
|
+
const rowCenterY = startY + i * LEGEND_ROW_H2 + LEGEND_ROW_H2 / 2;
|
|
1178
|
+
const textX = LEGEND_PAD2 + LEGEND_SWATCH_W2 + LEGEND_GAP2;
|
|
1179
|
+
return entry.swatch(LEGEND_PAD2, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT2 * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT2}" fill="${GLYPH_STROKE}">${xmlEscape(entry.label)}</text>`;
|
|
1150
1180
|
});
|
|
1151
1181
|
parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
|
|
1152
|
-
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label,
|
|
1153
|
-
width = Math.max(width,
|
|
1154
|
-
height = startY + entries.length *
|
|
1182
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT2)), 0);
|
|
1183
|
+
width = Math.max(width, LEGEND_PAD2 + LEGEND_SWATCH_W2 + LEGEND_GAP2 + widestLabel + LEGEND_PAD2);
|
|
1184
|
+
height = startY + entries.length * LEGEND_ROW_H2 + LEGEND_PAD2 / 2;
|
|
1155
1185
|
}
|
|
1156
1186
|
}
|
|
1157
1187
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
@@ -1228,11 +1258,11 @@ var SINGLE_RING_MAX = 8;
|
|
|
1228
1258
|
var NODE_STROKE = "#52525b";
|
|
1229
1259
|
var LABEL_FILL = "#3f3f46";
|
|
1230
1260
|
var EDGE_INK2 = "#71717a";
|
|
1231
|
-
var
|
|
1232
|
-
var
|
|
1233
|
-
var
|
|
1234
|
-
var
|
|
1235
|
-
var
|
|
1261
|
+
var LEGEND_ROW_H3 = 18;
|
|
1262
|
+
var LEGEND_PAD3 = 16;
|
|
1263
|
+
var LEGEND_SWATCH_W3 = 22;
|
|
1264
|
+
var LEGEND_GAP3 = 14;
|
|
1265
|
+
var LEGEND_FONT3 = 11;
|
|
1236
1266
|
var round = (n) => Math.round(n * 100) / 100;
|
|
1237
1267
|
function wrapTieLabel(displayLabel) {
|
|
1238
1268
|
const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));
|
|
@@ -1368,7 +1398,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1368
1398
|
const entries = [];
|
|
1369
1399
|
if (sats.some((s) => s.style === "plain")) {
|
|
1370
1400
|
entries.push({
|
|
1371
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1401
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W3}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="${EDGE_STROKE.plain.width}" stroke-opacity="${EDGE_STROKE.plain.opacity}"/>`,
|
|
1372
1402
|
label: labels.neutralTie
|
|
1373
1403
|
});
|
|
1374
1404
|
}
|
|
@@ -1378,28 +1408,973 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1378
1408
|
const ink = EDGE_STROKE[style];
|
|
1379
1409
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1380
1410
|
entries.push({
|
|
1381
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1411
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W3}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
|
|
1382
1412
|
label: labels.bondStyles[style]
|
|
1383
1413
|
});
|
|
1384
1414
|
}
|
|
1385
1415
|
if (sats.some((s) => s.tie.direction !== null)) {
|
|
1386
1416
|
entries.push({
|
|
1387
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1417
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W3 - 8}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="1.5" stroke-opacity="0.75"/>` + arrowHead(x + LEGEND_SWATCH_W3, y, 1, 0, 0.75),
|
|
1388
1418
|
label: labels.direction
|
|
1389
1419
|
});
|
|
1390
1420
|
}
|
|
1391
1421
|
if (entries.length > 0) {
|
|
1392
1422
|
const startY = height;
|
|
1393
1423
|
const rows = entries.map((entry, i) => {
|
|
1394
|
-
const rowCenterY = startY + i *
|
|
1395
|
-
const textX =
|
|
1396
|
-
return entry.swatch(
|
|
1424
|
+
const rowCenterY = startY + i * LEGEND_ROW_H3 + LEGEND_ROW_H3 / 2;
|
|
1425
|
+
const textX = LEGEND_PAD3 + LEGEND_SWATCH_W3 + LEGEND_GAP3;
|
|
1426
|
+
return entry.swatch(LEGEND_PAD3, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT3 * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT3}" fill="${NODE_STROKE}">${xmlEscape(entry.label)}</text>`;
|
|
1397
1427
|
});
|
|
1398
1428
|
parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
|
|
1399
|
-
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label,
|
|
1400
|
-
width = Math.max(width,
|
|
1401
|
-
height = startY + entries.length *
|
|
1429
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT3)), 0);
|
|
1430
|
+
width = Math.max(width, LEGEND_PAD3 + LEGEND_SWATCH_W3 + LEGEND_GAP3 + widestLabel + LEGEND_PAD3);
|
|
1431
|
+
height = startY + entries.length * LEGEND_ROW_H3 + LEGEND_PAD3 / 2;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const w = Math.ceil(width);
|
|
1435
|
+
const h = Math.ceil(height);
|
|
1436
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/fault-tree/types.ts
|
|
1440
|
+
var FAULT_TREE_EVENT_KINDS = [
|
|
1441
|
+
"intermediate",
|
|
1442
|
+
"basic",
|
|
1443
|
+
"undeveloped",
|
|
1444
|
+
"house",
|
|
1445
|
+
"conditioning",
|
|
1446
|
+
"transfer"
|
|
1447
|
+
];
|
|
1448
|
+
var GATE_TYPES = ["and", "or", "xor", "inhibit", "vote"];
|
|
1449
|
+
|
|
1450
|
+
// src/fault-tree/labels.ts
|
|
1451
|
+
var FAULT_TREE_TITLE_LABELS_EN = {
|
|
1452
|
+
gates: {
|
|
1453
|
+
and: "AND gate",
|
|
1454
|
+
or: "OR gate",
|
|
1455
|
+
xor: "Exclusive-OR gate",
|
|
1456
|
+
inhibit: "Inhibit gate",
|
|
1457
|
+
vote: "Voting gate"
|
|
1458
|
+
},
|
|
1459
|
+
condition: "condition"
|
|
1460
|
+
};
|
|
1461
|
+
var FAULT_TREE_SVG_LABELS_EN = {
|
|
1462
|
+
events: {
|
|
1463
|
+
intermediate: "Intermediate event",
|
|
1464
|
+
basic: "Basic event",
|
|
1465
|
+
undeveloped: "Undeveloped event",
|
|
1466
|
+
house: "External event (normally occurs)",
|
|
1467
|
+
conditioning: "Conditioning event",
|
|
1468
|
+
transfer: "Transfer (developed elsewhere)"
|
|
1469
|
+
},
|
|
1470
|
+
gates: {
|
|
1471
|
+
and: "AND gate (all inputs)",
|
|
1472
|
+
or: "OR gate (any input)",
|
|
1473
|
+
xor: "Exclusive-OR gate (exactly one input)",
|
|
1474
|
+
inhibit: "INHIBIT gate (input under condition)",
|
|
1475
|
+
vote: "Voting gate (k of n)"
|
|
1476
|
+
},
|
|
1477
|
+
ariaLabel: "Fault tree"
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/fault-tree/validate.ts
|
|
1481
|
+
var FaultTreeValidationError = class extends Error {
|
|
1482
|
+
issues;
|
|
1483
|
+
constructor(issues) {
|
|
1484
|
+
super(`invalid fault tree: ${issues.map((i) => i.message).join("; ")}`);
|
|
1485
|
+
this.name = "FaultTreeValidationError";
|
|
1486
|
+
this.issues = issues;
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
function listIds(ids) {
|
|
1490
|
+
if (ids.length === 1) return String(ids[0]);
|
|
1491
|
+
return `${ids.slice(0, -1).join(", ")} and ${ids[ids.length - 1]}`;
|
|
1492
|
+
}
|
|
1493
|
+
function sortIssues(issues) {
|
|
1494
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1495
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
1496
|
+
return [...unique.values()].sort(
|
|
1497
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
function faultTreeIssues(input) {
|
|
1501
|
+
if (input.events.length === 0 && input.gates.length === 0) return [];
|
|
1502
|
+
const issues = [];
|
|
1503
|
+
const push = (code, message) => {
|
|
1504
|
+
issues.push({ code, message });
|
|
1505
|
+
};
|
|
1506
|
+
const eventById = /* @__PURE__ */ new Map();
|
|
1507
|
+
const dupEventIds = /* @__PURE__ */ new Set();
|
|
1508
|
+
for (const e of input.events) {
|
|
1509
|
+
if (eventById.has(e.id)) dupEventIds.add(e.id);
|
|
1510
|
+
else eventById.set(e.id, e);
|
|
1511
|
+
}
|
|
1512
|
+
for (const id of [...dupEventIds].sort((a, b) => a - b)) {
|
|
1513
|
+
push("duplicate-id", `duplicate event id ${id}`);
|
|
1514
|
+
}
|
|
1515
|
+
const gateById = /* @__PURE__ */ new Map();
|
|
1516
|
+
const dupGateIds = /* @__PURE__ */ new Set();
|
|
1517
|
+
for (const g of input.gates) {
|
|
1518
|
+
if (gateById.has(g.id)) dupGateIds.add(g.id);
|
|
1519
|
+
else gateById.set(g.id, g);
|
|
1520
|
+
}
|
|
1521
|
+
for (const id of [...dupGateIds].sort((a, b) => a - b)) {
|
|
1522
|
+
push("duplicate-id", `duplicate gate id ${id}`);
|
|
1523
|
+
}
|
|
1524
|
+
if (issues.length > 0) {
|
|
1525
|
+
return sortIssues(issues);
|
|
1526
|
+
}
|
|
1527
|
+
const gates = [...gateById.values()].sort((a, b) => a.id - b.id);
|
|
1528
|
+
const top = eventById.get(input.topId);
|
|
1529
|
+
if (top === void 0 || top.kind !== "intermediate") {
|
|
1530
|
+
push("top-not-intermediate", `topId ${input.topId} is not an intermediate event`);
|
|
1531
|
+
}
|
|
1532
|
+
const conditioningAsInput = /* @__PURE__ */ new Set();
|
|
1533
|
+
for (const g of gates) {
|
|
1534
|
+
const out = eventById.get(g.eventId);
|
|
1535
|
+
if (out !== void 0 && out.kind === "transfer") {
|
|
1536
|
+
push("transfer-with-gate", `transfer event ${g.eventId} cannot have a gate`);
|
|
1537
|
+
} else if (out === void 0 || out.kind !== "intermediate") {
|
|
1538
|
+
push("gate-on-non-intermediate", `gate ${g.id} resolves non-intermediate event ${g.eventId}`);
|
|
1539
|
+
}
|
|
1540
|
+
if (g.type === "inhibit") {
|
|
1541
|
+
if (g.inputIds.length !== 1) push("gate-arity", `inhibit gate ${g.id} needs exactly 1 input`);
|
|
1542
|
+
} else if (g.inputIds.length < 2) {
|
|
1543
|
+
push("gate-arity", `${g.type} gate ${g.id} needs \u22652 inputs`);
|
|
1544
|
+
}
|
|
1545
|
+
if (g.type === "vote" && (!Number.isInteger(g.k) || g.k < 1 || g.k > g.inputIds.length)) {
|
|
1546
|
+
push("vote-threshold", `vote gate ${g.id}: k=${g.k} of ${g.inputIds.length}`);
|
|
1547
|
+
}
|
|
1548
|
+
if (g.type === "inhibit") {
|
|
1549
|
+
const cond = eventById.get(g.conditionId);
|
|
1550
|
+
if (cond === void 0 || cond.kind !== "conditioning") {
|
|
1551
|
+
push("inhibit-condition", `inhibit gate ${g.id} conditionId ${g.conditionId} is not a conditioning event`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
for (const inputId of g.inputIds) {
|
|
1555
|
+
const child = eventById.get(inputId);
|
|
1556
|
+
if (child === void 0) {
|
|
1557
|
+
push("unknown-input", `gate ${g.id} input ${inputId} is not a declared event`);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (inputId === input.topId) push("top-as-input", `top event ${input.topId} is used as a gate input`);
|
|
1561
|
+
if (child.kind === "conditioning") conditioningAsInput.add(inputId);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
for (const id of [...conditioningAsInput].sort((a, b) => a - b)) {
|
|
1565
|
+
push("conditioning-as-input", `conditioning event ${id} used as a gate input`);
|
|
1566
|
+
}
|
|
1567
|
+
const gatesByEvent = /* @__PURE__ */ new Map();
|
|
1568
|
+
for (const g of gates) {
|
|
1569
|
+
const arr = gatesByEvent.get(g.eventId) ?? [];
|
|
1570
|
+
arr.push(g.id);
|
|
1571
|
+
gatesByEvent.set(g.eventId, arr);
|
|
1572
|
+
}
|
|
1573
|
+
for (const [eventId, gateIds] of [...gatesByEvent.entries()].sort((a, b) => a[0] - b[0])) {
|
|
1574
|
+
if (gateIds.length > 1) {
|
|
1575
|
+
push("event-with-two-gates", `event ${eventId} is resolved by gates ${listIds(gateIds.sort((a, b) => a - b))}`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
for (const e of [...eventById.values()].sort((a, b) => a.id - b.id)) {
|
|
1579
|
+
if (e.kind === "intermediate" && !gatesByEvent.has(e.id)) {
|
|
1580
|
+
push("event-without-gate", `intermediate event ${e.id} has no gate \u2014 declare it kind "undeveloped" instead`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
const intermediateRefs = /* @__PURE__ */ new Map();
|
|
1584
|
+
for (const g of gates) {
|
|
1585
|
+
for (const inputId of g.inputIds) {
|
|
1586
|
+
if (eventById.get(inputId)?.kind !== "intermediate") continue;
|
|
1587
|
+
const arr = intermediateRefs.get(inputId) ?? [];
|
|
1588
|
+
arr.push(g.id);
|
|
1589
|
+
intermediateRefs.set(inputId, arr);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
for (const [eventId, gateIds] of [...intermediateRefs.entries()].sort((a, b) => a[0] - b[0])) {
|
|
1593
|
+
if (gateIds.length > 1) {
|
|
1594
|
+
push(
|
|
1595
|
+
"intermediate-reused",
|
|
1596
|
+
`intermediate event ${eventId} feeds gates ${listIds(gateIds.sort((a, b) => a - b))} \u2014 repeat it via a "transfer" event`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const GRAPH_BLOCKING = /* @__PURE__ */ new Set([
|
|
1601
|
+
"duplicate-id",
|
|
1602
|
+
"unknown-input",
|
|
1603
|
+
"gate-on-non-intermediate",
|
|
1604
|
+
"transfer-with-gate",
|
|
1605
|
+
"event-with-two-gates",
|
|
1606
|
+
"inhibit-condition",
|
|
1607
|
+
"top-not-intermediate"
|
|
1608
|
+
]);
|
|
1609
|
+
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
1610
|
+
const gateOf = (eventId) => gates.find((g) => g.eventId === eventId);
|
|
1611
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1612
|
+
const queue = [input.topId];
|
|
1613
|
+
while (queue.length > 0) {
|
|
1614
|
+
const id = queue.shift();
|
|
1615
|
+
if (reachable.has(id)) continue;
|
|
1616
|
+
reachable.add(id);
|
|
1617
|
+
const g = gateOf(id);
|
|
1618
|
+
if (g === void 0) continue;
|
|
1619
|
+
queue.push(...g.inputIds);
|
|
1620
|
+
if (g.type === "inhibit") queue.push(g.conditionId);
|
|
1621
|
+
}
|
|
1622
|
+
for (const e of [...eventById.values()].sort((a, b) => a.id - b.id)) {
|
|
1623
|
+
if (!reachable.has(e.id)) push("unreachable-event", `event ${e.id} is declared but unreachable from the top`);
|
|
1624
|
+
}
|
|
1625
|
+
const color = /* @__PURE__ */ new Map();
|
|
1626
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
1627
|
+
const dfs = (start) => {
|
|
1628
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
1629
|
+
color.set(start, 1);
|
|
1630
|
+
while (stack.length > 0) {
|
|
1631
|
+
const frame = stack[stack.length - 1];
|
|
1632
|
+
const children = gateOf(frame.id)?.inputIds ?? [];
|
|
1633
|
+
if (frame.nextChild >= children.length) {
|
|
1634
|
+
color.set(frame.id, 2);
|
|
1635
|
+
stack.pop();
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const child = children[frame.nextChild];
|
|
1639
|
+
frame.nextChild += 1;
|
|
1640
|
+
const c = color.get(child) ?? 0;
|
|
1641
|
+
if (c === 1) {
|
|
1642
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
1643
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
1644
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
1645
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
1646
|
+
const key = rotated.join(">");
|
|
1647
|
+
if (!seenCycles.has(key)) {
|
|
1648
|
+
seenCycles.add(key);
|
|
1649
|
+
push("cycle", `cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`);
|
|
1650
|
+
}
|
|
1651
|
+
} else if (c === 0) {
|
|
1652
|
+
color.set(child, 1);
|
|
1653
|
+
stack.push({ id: child, nextChild: 0 });
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
for (const e of [...eventById.values()].sort((a, b) => a.id - b.id)) {
|
|
1658
|
+
if ((color.get(e.id) ?? 0) === 0) dfs(e.id);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return sortIssues(issues);
|
|
1662
|
+
}
|
|
1663
|
+
function validateFaultTree(input) {
|
|
1664
|
+
const issues = faultTreeIssues(input);
|
|
1665
|
+
if (issues.length > 0) throw new FaultTreeValidationError(issues);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/fault-tree/layout.ts
|
|
1669
|
+
var FT_LABEL_FONT = 12;
|
|
1670
|
+
var FT_LABEL_LINE_H = 14;
|
|
1671
|
+
var CODE_FONT = 10;
|
|
1672
|
+
var LABEL_GAP = 6;
|
|
1673
|
+
var PADDING3 = 32;
|
|
1674
|
+
var H_GAP = 28;
|
|
1675
|
+
var CORRIDOR = 30;
|
|
1676
|
+
var STEM = 10;
|
|
1677
|
+
var COND_GAP = 14;
|
|
1678
|
+
var GATE_W = 44;
|
|
1679
|
+
var GATE_H = 36;
|
|
1680
|
+
var XOR_H = GATE_H + 5;
|
|
1681
|
+
var INHIBIT_W = 36;
|
|
1682
|
+
var INHIBIT_H = 46;
|
|
1683
|
+
var INHIBIT_CY = 23;
|
|
1684
|
+
var FT_STEM_ID_BASE = 1e6;
|
|
1685
|
+
var FT_DROP_ID_BASE = 2e6;
|
|
1686
|
+
var FT_BUS_ID_BASE = 3e6;
|
|
1687
|
+
var FT_RISER_ID_BASE = 4e6;
|
|
1688
|
+
var FT_CONDITION_ID_BASE = 5e6;
|
|
1689
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
1690
|
+
function wrapLeafLabel(displayLabel) {
|
|
1691
|
+
const perLine = Math.min(20, Math.max(12, Math.ceil(displayLabel.length / 2) + 2));
|
|
1692
|
+
return wrapLabel(displayLabel, perLine);
|
|
1693
|
+
}
|
|
1694
|
+
var CODE_REAL_HEADROOM = 1.15;
|
|
1695
|
+
var CODE_MARGIN = 2;
|
|
1696
|
+
function codeGlyphHalfAt(kind, y) {
|
|
1697
|
+
if (kind === "basic") {
|
|
1698
|
+
const dy = y - 22;
|
|
1699
|
+
return Math.sqrt(Math.max(0, 22 * 22 - dy * dy));
|
|
1700
|
+
}
|
|
1701
|
+
if (kind === "undeveloped") {
|
|
1702
|
+
return 24 * Math.max(0, 1 - Math.abs(y - 24) / 24);
|
|
1703
|
+
}
|
|
1704
|
+
if (kind === "house") {
|
|
1705
|
+
return y >= 16 ? 22 : 22 * y / 16;
|
|
1706
|
+
}
|
|
1707
|
+
return 22 * Math.max(0, Math.min(35, y)) / 35;
|
|
1708
|
+
}
|
|
1709
|
+
function codeBaselineHalfWidth(kind, glyphH) {
|
|
1710
|
+
return codeGlyphHalfAt(kind, glyphH / 2 + CODE_FONT * 0.32);
|
|
1711
|
+
}
|
|
1712
|
+
function maxCodeChars(kind, glyphH) {
|
|
1713
|
+
const half = codeBaselineHalfWidth(kind, glyphH);
|
|
1714
|
+
const perChar = CODE_FONT * CHAR_W * CODE_REAL_HEADROOM / 2;
|
|
1715
|
+
return Math.max(1, Math.floor((half - CODE_MARGIN) / perChar));
|
|
1716
|
+
}
|
|
1717
|
+
function measureEvent(event, maxLabelChars) {
|
|
1718
|
+
if (event.kind === "intermediate") {
|
|
1719
|
+
const lines2 = event.label === "" ? [] : wrapLabelBalanced(clampLabel(event.label, maxLabelChars), 3);
|
|
1720
|
+
const maxLineW = lines2.reduce((m, l) => Math.max(m, estimateTextWidth(l, FT_LABEL_FONT)), 0);
|
|
1721
|
+
const w = Math.max(96, maxLineW + 24);
|
|
1722
|
+
const h = lines2.length * FT_LABEL_LINE_H + 18;
|
|
1723
|
+
return { nodeW: w, nodeH: h, glyphW: w, glyphH: h, labelLines: lines2, code: null };
|
|
1724
|
+
}
|
|
1725
|
+
const rawCode = event.code === null || event.code === "" ? null : event.code;
|
|
1726
|
+
const lines = event.label === "" ? [] : wrapLeafLabel(clampLabel(event.label, maxLabelChars));
|
|
1727
|
+
const labelW = lines.reduce((m, l) => Math.max(m, estimateTextWidth(l, FT_LABEL_FONT)), 0);
|
|
1728
|
+
let glyphW;
|
|
1729
|
+
let glyphH;
|
|
1730
|
+
let code = rawCode;
|
|
1731
|
+
if (event.kind === "basic") {
|
|
1732
|
+
glyphW = 44;
|
|
1733
|
+
glyphH = 44;
|
|
1734
|
+
code = rawCode === null ? null : clampLabel(rawCode, maxCodeChars("basic", glyphH));
|
|
1735
|
+
} else if (event.kind === "undeveloped") {
|
|
1736
|
+
glyphW = 48;
|
|
1737
|
+
glyphH = 48;
|
|
1738
|
+
code = rawCode === null ? null : clampLabel(rawCode, maxCodeChars("undeveloped", glyphH));
|
|
1739
|
+
} else if (event.kind === "house") {
|
|
1740
|
+
glyphW = 44;
|
|
1741
|
+
glyphH = 40;
|
|
1742
|
+
code = rawCode === null ? null : clampLabel(rawCode, maxCodeChars("house", glyphH));
|
|
1743
|
+
} else if (event.kind === "conditioning") {
|
|
1744
|
+
code = rawCode === null ? null : clampLabel(rawCode, 6);
|
|
1745
|
+
const rx = Math.max(30, estimateTextWidth(code ?? "", CODE_FONT) / 2 + 8);
|
|
1746
|
+
glyphW = rx * 2;
|
|
1747
|
+
glyphH = 32;
|
|
1748
|
+
} else {
|
|
1749
|
+
glyphW = 44;
|
|
1750
|
+
glyphH = 35;
|
|
1751
|
+
code = rawCode === null ? null : clampLabel(rawCode, maxCodeChars("transfer", glyphH));
|
|
1752
|
+
}
|
|
1753
|
+
const nodeW = Math.max(glyphW, labelW);
|
|
1754
|
+
const nodeH = glyphH + (lines.length > 0 ? LABEL_GAP + lines.length * FT_LABEL_LINE_H : 0);
|
|
1755
|
+
return { nodeW, nodeH, glyphW, glyphH, labelLines: lines, code };
|
|
1756
|
+
}
|
|
1757
|
+
function eventTitle(event) {
|
|
1758
|
+
if (event.title !== void 0) return event.title;
|
|
1759
|
+
return event.code !== null && event.code !== "" ? `${event.code} \xB7 ${event.label}` : event.label;
|
|
1760
|
+
}
|
|
1761
|
+
function computeFaultTreeLayout(input, opts = {}) {
|
|
1762
|
+
if (input.events.length === 0 && input.gates.length === 0) {
|
|
1763
|
+
return { width: PADDING3 * 2, height: PADDING3 * 2, nodes: [], gates: [], elements: [] };
|
|
1764
|
+
}
|
|
1765
|
+
validateFaultTree(input);
|
|
1766
|
+
const titleLabels = opts.titleLabels ?? FAULT_TREE_TITLE_LABELS_EN;
|
|
1767
|
+
const eventById = new Map(input.events.map((e) => [e.id, e]));
|
|
1768
|
+
const gateByEvent = new Map(input.gates.map((g) => [g.eventId, g]));
|
|
1769
|
+
const seqByEvent = /* @__PURE__ */ new Map();
|
|
1770
|
+
const newInst = (event, gate, depth) => {
|
|
1771
|
+
const seq = seqByEvent.get(event.id) ?? 0;
|
|
1772
|
+
seqByEvent.set(event.id, seq + 1);
|
|
1773
|
+
const inst = {
|
|
1774
|
+
event,
|
|
1775
|
+
gate,
|
|
1776
|
+
children: [],
|
|
1777
|
+
cond: null,
|
|
1778
|
+
depth,
|
|
1779
|
+
m: measureEvent(event, opts.maxLabelChars),
|
|
1780
|
+
title: eventTitle(event),
|
|
1781
|
+
seq,
|
|
1782
|
+
spanL: 0,
|
|
1783
|
+
spanR: 0,
|
|
1784
|
+
offset: 0,
|
|
1785
|
+
cx: 0
|
|
1786
|
+
};
|
|
1787
|
+
return inst;
|
|
1788
|
+
};
|
|
1789
|
+
const build = (eventId, depth) => {
|
|
1790
|
+
const event = eventById.get(eventId);
|
|
1791
|
+
const gate = gateByEvent.get(eventId) ?? null;
|
|
1792
|
+
const inst = newInst(event, gate, depth);
|
|
1793
|
+
if (gate !== null) {
|
|
1794
|
+
if (gate.type === "inhibit") {
|
|
1795
|
+
inst.cond = newInst(eventById.get(gate.conditionId), null, depth);
|
|
1796
|
+
}
|
|
1797
|
+
for (const childId of gate.inputIds) inst.children.push(build(childId, depth + 1));
|
|
1798
|
+
}
|
|
1799
|
+
return inst;
|
|
1800
|
+
};
|
|
1801
|
+
const root = build(input.topId, 0);
|
|
1802
|
+
const pack = (inst) => {
|
|
1803
|
+
for (const c of inst.children) pack(c);
|
|
1804
|
+
const condW = inst.cond !== null ? COND_GAP + inst.cond.m.nodeW : 0;
|
|
1805
|
+
const selfL = inst.m.nodeW / 2;
|
|
1806
|
+
const selfR = inst.m.nodeW / 2 + condW;
|
|
1807
|
+
if (inst.children.length === 0) {
|
|
1808
|
+
inst.spanL = selfL;
|
|
1809
|
+
inst.spanR = selfR;
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const xs = [0];
|
|
1813
|
+
for (let i = 1; i < inst.children.length; i++) {
|
|
1814
|
+
xs.push(xs[i - 1] + inst.children[i - 1].spanR + H_GAP + inst.children[i].spanL);
|
|
1815
|
+
}
|
|
1816
|
+
const first = inst.children[0];
|
|
1817
|
+
const last2 = inst.children[inst.children.length - 1];
|
|
1818
|
+
const px = (xs[0] + xs[xs.length - 1]) / 2;
|
|
1819
|
+
inst.children.forEach((c, i) => c.offset = xs[i] - px);
|
|
1820
|
+
inst.spanL = Math.max(selfL, px - (xs[0] - first.spanL));
|
|
1821
|
+
inst.spanR = Math.max(selfR, xs[xs.length - 1] + last2.spanR - px);
|
|
1822
|
+
};
|
|
1823
|
+
pack(root);
|
|
1824
|
+
root.cx = PADDING3 + root.spanL;
|
|
1825
|
+
const placeX = (inst) => {
|
|
1826
|
+
for (const c of inst.children) {
|
|
1827
|
+
c.cx = inst.cx + c.offset;
|
|
1828
|
+
placeX(c);
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
placeX(root);
|
|
1832
|
+
const rowInsts = [];
|
|
1833
|
+
const visitRows = (inst) => {
|
|
1834
|
+
(rowInsts[inst.depth] ??= []).push(inst);
|
|
1835
|
+
for (const c of inst.children) visitRows(c);
|
|
1836
|
+
};
|
|
1837
|
+
visitRows(root);
|
|
1838
|
+
const depthCount = rowInsts.length;
|
|
1839
|
+
const gateExtent = (inst) => {
|
|
1840
|
+
const g = inst.gate;
|
|
1841
|
+
if (g.type === "xor") return XOR_H;
|
|
1842
|
+
if (g.type === "inhibit") {
|
|
1843
|
+
const condBottom = INHIBIT_CY - 16 + inst.cond.m.nodeH;
|
|
1844
|
+
return Math.max(INHIBIT_H, condBottom);
|
|
1845
|
+
}
|
|
1846
|
+
return GATE_H;
|
|
1847
|
+
};
|
|
1848
|
+
const rowH = [];
|
|
1849
|
+
const gateZone = [];
|
|
1850
|
+
for (let d = 0; d < depthCount; d++) {
|
|
1851
|
+
const insts = rowInsts[d];
|
|
1852
|
+
rowH.push(insts.reduce((m, i) => Math.max(m, i.m.nodeH), 0));
|
|
1853
|
+
const gated = insts.filter((i) => i.gate !== null);
|
|
1854
|
+
gateZone.push(gated.length === 0 ? 0 : STEM + gated.reduce((m, i) => Math.max(m, gateExtent(i)), 0));
|
|
1855
|
+
}
|
|
1856
|
+
const rowTop = [PADDING3];
|
|
1857
|
+
for (let d = 0; d < depthCount - 1; d++) {
|
|
1858
|
+
rowTop.push(rowTop[d] + rowH[d] + gateZone[d] + CORRIDOR);
|
|
1859
|
+
}
|
|
1860
|
+
const busY = (d) => rowTop[d + 1] - CORRIDOR / 2;
|
|
1861
|
+
const width = Math.ceil(PADDING3 * 2 + root.spanL + root.spanR);
|
|
1862
|
+
const last = depthCount - 1;
|
|
1863
|
+
const height = Math.ceil(rowTop[last] + rowH[last] + gateZone[last] + PADDING3);
|
|
1864
|
+
const instanceOf = (inst) => (seqByEvent.get(inst.event.id) ?? 0) > 1 ? inst.seq : null;
|
|
1865
|
+
const nodes = [];
|
|
1866
|
+
const gates = [];
|
|
1867
|
+
const elements = [];
|
|
1868
|
+
const gateTitle = (g) => g.type === "vote" ? `${titleLabels.gates.vote} ${g.k}/${g.inputIds.length}` : titleLabels.gates[g.type];
|
|
1869
|
+
const pushNode = (inst, top) => {
|
|
1870
|
+
const isRect = inst.event.kind === "intermediate";
|
|
1871
|
+
const labelTop = !isRect && inst.m.labelLines.length > 0 ? top + inst.m.glyphH + LABEL_GAP : null;
|
|
1872
|
+
nodes.push({
|
|
1873
|
+
eventId: inst.event.id,
|
|
1874
|
+
kind: inst.event.kind,
|
|
1875
|
+
instance: instanceOf(inst),
|
|
1876
|
+
cx: round2(inst.cx),
|
|
1877
|
+
top: round2(top),
|
|
1878
|
+
nodeW: round2(inst.m.nodeW),
|
|
1879
|
+
nodeH: round2(inst.m.nodeH),
|
|
1880
|
+
glyphW: round2(inst.m.glyphW),
|
|
1881
|
+
glyphH: round2(inst.m.glyphH),
|
|
1882
|
+
labelLines: inst.m.labelLines,
|
|
1883
|
+
labelTop: labelTop === null ? null : round2(labelTop),
|
|
1884
|
+
code: inst.m.code,
|
|
1885
|
+
depth: inst.depth,
|
|
1886
|
+
title: inst.title
|
|
1887
|
+
});
|
|
1888
|
+
};
|
|
1889
|
+
const emit = (inst) => {
|
|
1890
|
+
const d = inst.depth;
|
|
1891
|
+
pushNode(inst, rowTop[d]);
|
|
1892
|
+
const g = inst.gate;
|
|
1893
|
+
if (g === null) return;
|
|
1894
|
+
const gateTop = rowTop[d] + rowH[d] + STEM;
|
|
1895
|
+
const glyphW = g.type === "inhibit" ? INHIBIT_W : GATE_W;
|
|
1896
|
+
const glyphH = g.type === "xor" ? XOR_H : g.type === "inhibit" ? INHIBIT_H : GATE_H;
|
|
1897
|
+
const title = gateTitle(g);
|
|
1898
|
+
gates.push({
|
|
1899
|
+
gateId: g.id,
|
|
1900
|
+
type: g.type,
|
|
1901
|
+
cx: round2(inst.cx),
|
|
1902
|
+
top: round2(gateTop),
|
|
1903
|
+
glyphW,
|
|
1904
|
+
glyphH,
|
|
1905
|
+
title,
|
|
1906
|
+
voteText: g.type === "vote" ? `${g.k}/${g.inputIds.length}` : null
|
|
1907
|
+
});
|
|
1908
|
+
if (inst.cond !== null) {
|
|
1909
|
+
const cond = inst.cond;
|
|
1910
|
+
const nodeLeft = inst.cx + INHIBIT_W / 2 + COND_GAP;
|
|
1911
|
+
cond.cx = nodeLeft + cond.m.nodeW / 2;
|
|
1912
|
+
const condTop = gateTop + INHIBIT_CY - 16;
|
|
1913
|
+
pushNode(cond, condTop);
|
|
1914
|
+
const ovalLeft = cond.cx - cond.m.glyphW / 2;
|
|
1915
|
+
elements.push({
|
|
1916
|
+
edgeId: FT_CONDITION_ID_BASE + g.id,
|
|
1917
|
+
kind: "condition",
|
|
1918
|
+
points: [
|
|
1919
|
+
{ x: round2(inst.cx + INHIBIT_W / 2), y: round2(gateTop + INHIBIT_CY) },
|
|
1920
|
+
{ x: round2(ovalLeft), y: round2(gateTop + INHIBIT_CY) }
|
|
1921
|
+
],
|
|
1922
|
+
instance: null,
|
|
1923
|
+
title: titleLabels.condition
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
elements.push({
|
|
1927
|
+
edgeId: FT_STEM_ID_BASE + g.id,
|
|
1928
|
+
kind: "stem",
|
|
1929
|
+
points: [
|
|
1930
|
+
{ x: round2(inst.cx), y: round2(rowTop[d] + inst.m.nodeH) },
|
|
1931
|
+
{ x: round2(inst.cx), y: round2(gateTop) }
|
|
1932
|
+
],
|
|
1933
|
+
instance: null,
|
|
1934
|
+
title
|
|
1935
|
+
});
|
|
1936
|
+
const by = busY(d);
|
|
1937
|
+
elements.push({
|
|
1938
|
+
edgeId: FT_DROP_ID_BASE + g.id,
|
|
1939
|
+
kind: "drop",
|
|
1940
|
+
points: [
|
|
1941
|
+
{ x: round2(inst.cx), y: round2(gateTop + glyphH) },
|
|
1942
|
+
{ x: round2(inst.cx), y: round2(by) }
|
|
1943
|
+
],
|
|
1944
|
+
instance: null,
|
|
1945
|
+
title
|
|
1946
|
+
});
|
|
1947
|
+
if (inst.children.length > 1) {
|
|
1948
|
+
const xs = inst.children.map((c) => c.cx);
|
|
1949
|
+
elements.push({
|
|
1950
|
+
edgeId: FT_BUS_ID_BASE + g.id,
|
|
1951
|
+
kind: "bus",
|
|
1952
|
+
points: [
|
|
1953
|
+
{ x: round2(Math.min(...xs)), y: round2(by) },
|
|
1954
|
+
{ x: round2(Math.max(...xs)), y: round2(by) }
|
|
1955
|
+
],
|
|
1956
|
+
instance: null,
|
|
1957
|
+
title
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
for (const c of inst.children) {
|
|
1961
|
+
elements.push({
|
|
1962
|
+
edgeId: FT_RISER_ID_BASE + c.event.id,
|
|
1963
|
+
kind: "riser",
|
|
1964
|
+
points: [
|
|
1965
|
+
{ x: round2(c.cx), y: round2(by) },
|
|
1966
|
+
{ x: round2(c.cx), y: round2(rowTop[d + 1]) }
|
|
1967
|
+
],
|
|
1968
|
+
instance: instanceOf(c),
|
|
1969
|
+
title
|
|
1970
|
+
});
|
|
1402
1971
|
}
|
|
1972
|
+
for (const c of inst.children) emit(c);
|
|
1973
|
+
};
|
|
1974
|
+
emit(root);
|
|
1975
|
+
return { width, height, nodes, gates, elements };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/fault-tree/svg.ts
|
|
1979
|
+
var GLYPH_STROKE2 = "#52525b";
|
|
1980
|
+
var LABEL_FILL2 = "#3f3f46";
|
|
1981
|
+
var EDGE_INK3 = "#71717a";
|
|
1982
|
+
var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="2"`;
|
|
1983
|
+
var round3 = (n) => Math.round(n * 100) / 100;
|
|
1984
|
+
function eventGlyph(n) {
|
|
1985
|
+
const cx = n.cx;
|
|
1986
|
+
const top = n.top;
|
|
1987
|
+
if (n.kind === "intermediate") {
|
|
1988
|
+
return `<rect x="${round3(cx - n.nodeW / 2)}" y="${top}" width="${n.nodeW}" height="${n.nodeH}" rx="2" ${GLYPH_ATTRS}/>`;
|
|
1989
|
+
}
|
|
1990
|
+
if (n.kind === "basic") {
|
|
1991
|
+
return `<circle cx="${cx}" cy="${round3(top + 22)}" r="22" ${GLYPH_ATTRS}/>`;
|
|
1992
|
+
}
|
|
1993
|
+
if (n.kind === "undeveloped") {
|
|
1994
|
+
const pts2 = `${cx},${top} ${round3(cx + 24)},${round3(top + 24)} ${cx},${round3(top + 48)} ${round3(cx - 24)},${round3(top + 24)}`;
|
|
1995
|
+
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
1996
|
+
}
|
|
1997
|
+
if (n.kind === "house") {
|
|
1998
|
+
const yB = round3(top + 40);
|
|
1999
|
+
const eave = round3(top + 16);
|
|
2000
|
+
const pts2 = `${round3(cx - 22)},${yB} ${round3(cx - 22)},${eave} ${cx},${top} ${round3(cx + 22)},${eave} ${round3(cx + 22)},${yB}`;
|
|
2001
|
+
return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
|
|
2002
|
+
}
|
|
2003
|
+
if (n.kind === "conditioning") {
|
|
2004
|
+
return `<ellipse cx="${cx}" cy="${round3(top + 16)}" rx="${round3(n.glyphW / 2)}" ry="16" ${GLYPH_ATTRS}/>`;
|
|
2005
|
+
}
|
|
2006
|
+
const pts = `${cx},${top} ${round3(cx + 22)},${round3(top + 35)} ${round3(cx - 22)},${round3(top + 35)}`;
|
|
2007
|
+
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2008
|
+
}
|
|
2009
|
+
function nodeSvg(n) {
|
|
2010
|
+
const pieces = [`<title>${xmlEscape(n.title)}</title>`, eventGlyph(n)];
|
|
2011
|
+
if (n.code !== null && n.kind !== "intermediate") {
|
|
2012
|
+
pieces.push(
|
|
2013
|
+
`<text x="${n.cx}" y="${round3(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>`
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
if (n.labelLines.length > 0) {
|
|
2017
|
+
const firstBaseline = n.labelTop === null ? round3(n.top + 19) : round3(n.labelTop + 10);
|
|
2018
|
+
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round3(firstBaseline + i * FT_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
|
|
2019
|
+
pieces.push(
|
|
2020
|
+
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${FT_LABEL_FONT}" fill="${LABEL_FILL2}">${tspans}</text>`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
const instance = n.instance === null ? "" : ` data-instance="${n.instance}"`;
|
|
2024
|
+
return `<g data-node-id="e${n.eventId}"${instance}>${pieces.join("")}</g>`;
|
|
2025
|
+
}
|
|
2026
|
+
function orBodyPath(cx, top, yB) {
|
|
2027
|
+
return `M ${round3(cx - 22)} ${yB} Q ${cx} ${round3(yB - 14)} ${round3(cx + 22)} ${yB} Q ${round3(cx + 22)} ${round3(top + 14)} ${cx} ${top} Q ${round3(cx - 22)} ${round3(top + 14)} ${round3(cx - 22)} ${yB} Z`;
|
|
2028
|
+
}
|
|
2029
|
+
function gateGlyph(g) {
|
|
2030
|
+
const cx = g.cx;
|
|
2031
|
+
const top = g.top;
|
|
2032
|
+
const yB = round3(top + 36);
|
|
2033
|
+
if (g.type === "and") {
|
|
2034
|
+
const d = `M ${round3(cx - 22)} ${yB} L ${round3(cx - 22)} ${round3(yB - 14)} A 22 22 0 0 1 ${round3(cx + 22)} ${round3(yB - 14)} L ${round3(cx + 22)} ${yB} Z`;
|
|
2035
|
+
return `<path d="${d}" ${GLYPH_ATTRS}/>`;
|
|
2036
|
+
}
|
|
2037
|
+
if (g.type === "or") {
|
|
2038
|
+
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/>`;
|
|
2039
|
+
}
|
|
2040
|
+
if (g.type === "xor") {
|
|
2041
|
+
const arc = `M ${round3(cx - 22)} ${round3(yB + 5)} Q ${cx} ${round3(yB - 9)} ${round3(cx + 22)} ${round3(yB + 5)}`;
|
|
2042
|
+
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><path d="${arc}" ${GLYPH_ATTRS}/>`;
|
|
2043
|
+
}
|
|
2044
|
+
if (g.type === "inhibit") {
|
|
2045
|
+
const pts = `${cx},${top} ${round3(cx + 18)},${round3(top + 12)} ${round3(cx + 18)},${round3(top + 34)} ${cx},${round3(top + 46)} ${round3(cx - 18)},${round3(top + 34)} ${round3(cx - 18)},${round3(top + 12)}`;
|
|
2046
|
+
return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
|
|
2047
|
+
}
|
|
2048
|
+
return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><text x="${cx}" y="${round3(yB - 10)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL2}">${xmlEscape(g.voteText ?? "")}</text>`;
|
|
2049
|
+
}
|
|
2050
|
+
function gateSvg(g) {
|
|
2051
|
+
return `<g data-node-id="g${g.gateId}"><title>${xmlEscape(g.title)}</title>${gateGlyph(g)}</g>`;
|
|
2052
|
+
}
|
|
2053
|
+
function elementSvg2(el) {
|
|
2054
|
+
const a = el.points[0];
|
|
2055
|
+
const b = el.points[1];
|
|
2056
|
+
const instance = el.instance === null ? "" : ` data-instance="${el.instance}"`;
|
|
2057
|
+
return `<g data-edge-id="${el.edgeId}"${instance}><title>${xmlEscape(el.title)}</title><line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK3}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
|
|
2058
|
+
}
|
|
2059
|
+
var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="1.5"`;
|
|
2060
|
+
function miniEventSwatch(kind, x, y) {
|
|
2061
|
+
const cx = round3(x + LEGEND_SWATCH_W / 2);
|
|
2062
|
+
if (kind === "intermediate") {
|
|
2063
|
+
return `<rect x="${round3(cx - 7)}" y="${round3(y - 4.5)}" width="14" height="9" rx="1" ${MINI_ATTRS}/>`;
|
|
2064
|
+
}
|
|
2065
|
+
if (kind === "basic") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/>`;
|
|
2066
|
+
if (kind === "undeveloped") {
|
|
2067
|
+
return `<polygon points="${cx},${round3(y - 7)} ${round3(cx + 7)},${y} ${cx},${round3(y + 7)} ${round3(cx - 7)},${y}" ${MINI_ATTRS}/>`;
|
|
2068
|
+
}
|
|
2069
|
+
if (kind === "house") {
|
|
2070
|
+
return `<polygon points="${round3(cx - 6)},${round3(y + 5.5)} ${round3(cx - 6)},${round3(y - 1)} ${cx},${round3(y - 5.5)} ${round3(cx + 6)},${round3(y - 1)} ${round3(cx + 6)},${round3(y + 5.5)}" ${MINI_ATTRS}/>`;
|
|
2071
|
+
}
|
|
2072
|
+
if (kind === "conditioning") return `<ellipse cx="${cx}" cy="${y}" rx="9" ry="5.5" ${MINI_ATTRS}/>`;
|
|
2073
|
+
return `<polygon points="${cx},${round3(y - 5)} ${round3(cx + 6)},${round3(y + 5)} ${round3(cx - 6)},${round3(y + 5)}" ${MINI_ATTRS}/>`;
|
|
2074
|
+
}
|
|
2075
|
+
function miniOrPath(cx, y) {
|
|
2076
|
+
return `M ${round3(cx - 7)} ${round3(y + 5.5)} Q ${cx} ${round3(y + 1)} ${round3(cx + 7)} ${round3(y + 5.5)} Q ${round3(cx + 7)} ${round3(y - 1.5)} ${cx} ${round3(y - 5.5)} Q ${round3(cx - 7)} ${round3(y - 1.5)} ${round3(cx - 7)} ${round3(y + 5.5)} Z`;
|
|
2077
|
+
}
|
|
2078
|
+
function miniGateSwatch(type, x, y) {
|
|
2079
|
+
const cx = round3(x + LEGEND_SWATCH_W / 2);
|
|
2080
|
+
if (type === "and") {
|
|
2081
|
+
const d = `M ${round3(cx - 7)} ${round3(y + 5.5)} L ${round3(cx - 7)} ${round3(y + 1.5)} A 7 7 0 0 1 ${round3(cx + 7)} ${round3(y + 1.5)} L ${round3(cx + 7)} ${round3(y + 5.5)} Z`;
|
|
2082
|
+
return `<path d="${d}" ${MINI_ATTRS}/>`;
|
|
2083
|
+
}
|
|
2084
|
+
if (type === "xor") {
|
|
2085
|
+
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/><path d="M ${round3(cx - 7)} ${round3(y + 7.5)} Q ${cx} ${round3(y + 3)} ${round3(cx + 7)} ${round3(y + 7.5)}" ${MINI_ATTRS}/>`;
|
|
2086
|
+
}
|
|
2087
|
+
if (type === "inhibit") {
|
|
2088
|
+
return `<polygon points="${cx},${round3(y - 5.5)} ${round3(cx + 4.5)},${round3(y - 2.5)} ${round3(cx + 4.5)},${round3(y + 2.5)} ${cx},${round3(y + 5.5)} ${round3(cx - 4.5)},${round3(y + 2.5)} ${round3(cx - 4.5)},${round3(y - 2.5)}" ${MINI_ATTRS}/>`;
|
|
2089
|
+
}
|
|
2090
|
+
return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/>`;
|
|
2091
|
+
}
|
|
2092
|
+
function faultTreeLayoutSvg(layout, opts = {}) {
|
|
2093
|
+
const labels = opts.labels ?? FAULT_TREE_SVG_LABELS_EN;
|
|
2094
|
+
const parts = [];
|
|
2095
|
+
for (const el of layout.elements) parts.push(elementSvg2(el));
|
|
2096
|
+
for (const g of layout.gates) parts.push(gateSvg(g));
|
|
2097
|
+
for (const n of layout.nodes) parts.push(nodeSvg(n));
|
|
2098
|
+
let width = layout.width;
|
|
2099
|
+
let height = layout.height;
|
|
2100
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
2101
|
+
const kindsUsed = new Set(layout.nodes.map((n) => n.kind));
|
|
2102
|
+
const typesUsed = new Set(layout.gates.map((g) => g.type));
|
|
2103
|
+
const entries = [];
|
|
2104
|
+
for (const kind of FAULT_TREE_EVENT_KINDS) {
|
|
2105
|
+
if (!kindsUsed.has(kind)) continue;
|
|
2106
|
+
entries.push({ swatch: (x, y) => miniEventSwatch(kind, x, y), label: labels.events[kind] });
|
|
2107
|
+
}
|
|
2108
|
+
for (const type of GATE_TYPES) {
|
|
2109
|
+
if (!typesUsed.has(type)) continue;
|
|
2110
|
+
entries.push({ swatch: (x, y) => miniGateSwatch(type, x, y), label: labels.gates[type] });
|
|
2111
|
+
}
|
|
2112
|
+
const block = legendBlock(entries, layout.height);
|
|
2113
|
+
if (block.svg !== "") {
|
|
2114
|
+
parts.push(block.svg);
|
|
2115
|
+
width = Math.max(width, block.width);
|
|
2116
|
+
height = block.height;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const w = Math.ceil(width);
|
|
2120
|
+
const h = Math.ceil(height);
|
|
2121
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/fault-tree/render.ts
|
|
2125
|
+
function faultTreeSvg(input, opts = {}) {
|
|
2126
|
+
const layout = computeFaultTreeLayout(input, {
|
|
2127
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
2128
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
2129
|
+
});
|
|
2130
|
+
const svg = faultTreeLayoutSvg(layout, {
|
|
2131
|
+
...opts.legend === false ? { legend: false } : {},
|
|
2132
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
2133
|
+
});
|
|
2134
|
+
return { svg, layout };
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/fishbone/render.ts
|
|
2138
|
+
var FISHBONE_LABELS_EN = {
|
|
2139
|
+
cause: "Cause",
|
|
2140
|
+
subCause: "Sub-cause",
|
|
2141
|
+
ariaLabel: "Cause-and-effect diagram (Ishikawa)"
|
|
2142
|
+
};
|
|
2143
|
+
var FishboneValidationError = class extends Error {
|
|
2144
|
+
issues;
|
|
2145
|
+
constructor(issues) {
|
|
2146
|
+
super(`invalid fishbone: ${issues.map((i) => i.message).join("; ")}`);
|
|
2147
|
+
this.name = "FishboneValidationError";
|
|
2148
|
+
this.issues = issues;
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
function duplicateIds(ids) {
|
|
2152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2153
|
+
const dups = /* @__PURE__ */ new Set();
|
|
2154
|
+
for (const id of ids) {
|
|
2155
|
+
if (seen.has(id)) dups.add(id);
|
|
2156
|
+
seen.add(id);
|
|
2157
|
+
}
|
|
2158
|
+
return [...dups].sort((a, b) => a - b);
|
|
2159
|
+
}
|
|
2160
|
+
function validateIds(input) {
|
|
2161
|
+
const issues = [];
|
|
2162
|
+
const collect = (noun, ids) => {
|
|
2163
|
+
for (const id of duplicateIds(ids)) {
|
|
2164
|
+
issues.push({ code: "duplicate-id", message: `duplicate ${noun} id ${id}` });
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
collect("category", input.categories.map((c) => c.id));
|
|
2168
|
+
collect("cause", input.categories.flatMap((c) => c.causes.map((k) => k.id)));
|
|
2169
|
+
collect(
|
|
2170
|
+
"sub-cause",
|
|
2171
|
+
input.categories.flatMap((c) => c.causes.flatMap((k) => k.subCauses.map((s) => s.id)))
|
|
2172
|
+
);
|
|
2173
|
+
if (issues.length > 0) throw new FishboneValidationError(issues);
|
|
2174
|
+
}
|
|
2175
|
+
var TAN60 = 1.7320508075688772;
|
|
2176
|
+
var SIN60 = 0.8660254037844386;
|
|
2177
|
+
var COS60 = 0.5;
|
|
2178
|
+
var PADDING4 = 32;
|
|
2179
|
+
var ROW_GAP = 12;
|
|
2180
|
+
var BONE_GAP = 36;
|
|
2181
|
+
var CAT_PAD_X = 12;
|
|
2182
|
+
var CAT_PAD_Y = 8;
|
|
2183
|
+
var CAT_GAP = 12;
|
|
2184
|
+
var TAIL_EXTRA = 44;
|
|
2185
|
+
var HEAD_GAP = 24;
|
|
2186
|
+
var HEAD_PAD_X = 14;
|
|
2187
|
+
var HEAD_PAD_Y = 10;
|
|
2188
|
+
var SUB_PER_LINE = 18;
|
|
2189
|
+
var ASCENT_12 = 11;
|
|
2190
|
+
var DESCENT_12 = 3;
|
|
2191
|
+
function verticalMetrics(fontSize) {
|
|
2192
|
+
const ascent = Math.ceil(ASCENT_12 * fontSize / 12);
|
|
2193
|
+
const descent = Math.ceil(DESCENT_12 * fontSize / 12);
|
|
2194
|
+
const lineH = ascent + descent;
|
|
2195
|
+
const twigGap = descent + 1;
|
|
2196
|
+
return {
|
|
2197
|
+
ascent,
|
|
2198
|
+
lineH,
|
|
2199
|
+
twigGap,
|
|
2200
|
+
sV: 2 * lineH + twigGap + 8,
|
|
2201
|
+
spineClear: 2 * lineH + twigGap,
|
|
2202
|
+
boneClear: Math.ceil((2 * lineH + twigGap) / TAN60) + 25,
|
|
2203
|
+
subGap: lineH
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
var EDGE_INK4 = "#71717a";
|
|
2207
|
+
var BOX_STROKE = "#52525b";
|
|
2208
|
+
var LABEL_FILL3 = "#3f3f46";
|
|
2209
|
+
var SPINE_W = 2.5;
|
|
2210
|
+
var SPINE_OP = 0.85;
|
|
2211
|
+
var BONE_W = 2;
|
|
2212
|
+
var BONE_OP = 0.8;
|
|
2213
|
+
var TWIG_W = 1.5;
|
|
2214
|
+
var TWIG_OP = 0.75;
|
|
2215
|
+
var SUB_W = 1.2;
|
|
2216
|
+
var SUB_OP = 0.7;
|
|
2217
|
+
var round4 = (n) => Math.round(n * 100) / 100;
|
|
2218
|
+
function arrowHead2(tipX, tipY, ux, uy, opacity) {
|
|
2219
|
+
const LEN = 9;
|
|
2220
|
+
const HALF_W = 4.5;
|
|
2221
|
+
const bx = tipX - ux * LEN;
|
|
2222
|
+
const by = tipY - uy * LEN;
|
|
2223
|
+
const px = -uy;
|
|
2224
|
+
const py = ux;
|
|
2225
|
+
const points = [
|
|
2226
|
+
`${round4(tipX)},${round4(tipY)}`,
|
|
2227
|
+
`${round4(bx + px * HALF_W)},${round4(by + py * HALF_W)}`,
|
|
2228
|
+
`${round4(bx - px * HALF_W)},${round4(by - py * HALF_W)}`
|
|
2229
|
+
].join(" ");
|
|
2230
|
+
return `<polygon points="${points}" fill="${EDGE_INK4}" fill-opacity="${opacity}"/>`;
|
|
2231
|
+
}
|
|
2232
|
+
function lineEl(x1, y1, x2, y2, w, op) {
|
|
2233
|
+
return `<line x1="${round4(x1)}" y1="${round4(y1)}" x2="${round4(x2)}" y2="${round4(y2)}" stroke="${EDGE_INK4}" stroke-width="${w}" stroke-opacity="${op}"/>`;
|
|
2234
|
+
}
|
|
2235
|
+
function fishboneSvg(input, opts = {}) {
|
|
2236
|
+
validateIds(input);
|
|
2237
|
+
const fontSize = opts.fontSize ?? 12;
|
|
2238
|
+
const { ascent, lineH, twigGap, sV, spineClear, boneClear, subGap } = verticalMetrics(fontSize);
|
|
2239
|
+
const labels = opts.labels ?? FISHBONE_LABELS_EN;
|
|
2240
|
+
const arrows = opts.arrowheads !== false;
|
|
2241
|
+
const bones = input.categories.map((category, idx) => {
|
|
2242
|
+
let cursor = spineClear;
|
|
2243
|
+
const bands = category.causes.map((cause) => {
|
|
2244
|
+
const lines = wrapLabelBalanced(clampLabel(cause.label, opts.maxLabelChars));
|
|
2245
|
+
const labelW = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize)));
|
|
2246
|
+
const blockH = lines.length * lineH;
|
|
2247
|
+
const offset = cursor;
|
|
2248
|
+
const bx = -offset / TAN60;
|
|
2249
|
+
const subs = [];
|
|
2250
|
+
for (const sub of cause.subCauses) {
|
|
2251
|
+
const line = wrapLabel(clampLabel(sub.label, opts.maxLabelChars), SUB_PER_LINE, 1)[0];
|
|
2252
|
+
const w2 = estimateTextWidth(line, fontSize);
|
|
2253
|
+
const prev = subs.length > 0 ? subs[subs.length - 1] : null;
|
|
2254
|
+
const sx = prev === null ? bx - (sV + lineH) / TAN60 - 10 - w2 / 2 : prev.sx - (prev.w + w2) * 0.6 - subGap;
|
|
2255
|
+
subs.push({ sub, line, w: w2, sx, outerX: sx - sV / TAN60 });
|
|
2256
|
+
}
|
|
2257
|
+
const last = subs.length > 0 ? subs[subs.length - 1] : null;
|
|
2258
|
+
const subSpan = last === null ? 0 : bx - (last.outerX - last.w / 2);
|
|
2259
|
+
const twigLen = last === null ? labelW + boneClear : Math.max(labelW + boneClear, subSpan + boneClear, labelW + (bx - last.outerX) + 10);
|
|
2260
|
+
const bandH = Math.max(blockH + twigGap, subs.length > 0 ? sV + lineH + twigGap : 0) + ROW_GAP;
|
|
2261
|
+
cursor += bandH;
|
|
2262
|
+
return { cause, lines, labelW, blockH, offset, bandH, bx, freeEnd: bx - twigLen, subs };
|
|
2263
|
+
});
|
|
2264
|
+
const B = cursor + 16;
|
|
2265
|
+
const catLines = wrapLabelBalanced(clampLabel(category.label, opts.maxLabelChars));
|
|
2266
|
+
const boxW = Math.max(...catLines.map((l) => estimateTextWidth(l, fontSize))) + CAT_PAD_X * 2;
|
|
2267
|
+
const boxH = catLines.length * lineH + CAT_PAD_Y * 2;
|
|
2268
|
+
const tipX = -B / TAN60;
|
|
2269
|
+
return {
|
|
2270
|
+
category,
|
|
2271
|
+
up: idx % 2 === 0,
|
|
2272
|
+
bands,
|
|
2273
|
+
B,
|
|
2274
|
+
catLines,
|
|
2275
|
+
boxW,
|
|
2276
|
+
boxH,
|
|
2277
|
+
// Left extent: the category box's left edge or the deepest twig free end —
|
|
2278
|
+
// every label sits AT or right of its twig's free end, every sub label sits
|
|
2279
|
+
// right of the free end too (twigLen ≥ subSpan + boneClear).
|
|
2280
|
+
relL: Math.max(B / TAN60 + boxW / 2, ...bands.map((b) => -b.freeEnd)),
|
|
2281
|
+
// Right extent: a wide category box on a short bone may overhang the attach.
|
|
2282
|
+
relR: Math.max(0, boxW / 2 + tipX),
|
|
2283
|
+
ax: 0
|
|
2284
|
+
};
|
|
2285
|
+
});
|
|
2286
|
+
const cursors = { top: 0, bottom: 0 };
|
|
2287
|
+
for (const bone of bones) {
|
|
2288
|
+
const side = bone.up ? "top" : "bottom";
|
|
2289
|
+
bone.ax = cursors[side] - bone.relR;
|
|
2290
|
+
cursors[side] = bone.ax - bone.relL - BONE_GAP;
|
|
2291
|
+
}
|
|
2292
|
+
const effLines = wrapLabelBalanced(clampLabel(input.effectLabel, opts.maxLabelChars));
|
|
2293
|
+
const headW = Math.max(...effLines.map((l) => estimateTextWidth(l, fontSize))) + HEAD_PAD_X * 2;
|
|
2294
|
+
const headH = effLines.length * lineH + HEAD_PAD_Y * 2;
|
|
2295
|
+
const headLeft = bones.length > 0 ? HEAD_GAP : 0;
|
|
2296
|
+
const tailX = bones.length > 0 ? Math.min(...bones.map((b) => b.ax - b.relL)) - TAIL_EXTRA : headLeft - (TAIL_EXTRA + HEAD_GAP);
|
|
2297
|
+
let minY = -headH / 2;
|
|
2298
|
+
let maxY = headH / 2;
|
|
2299
|
+
for (const bone of bones) {
|
|
2300
|
+
const reach = bone.B + CAT_GAP + bone.boxH;
|
|
2301
|
+
if (bone.up) minY = Math.min(minY, -reach);
|
|
2302
|
+
else maxY = Math.max(maxY, reach);
|
|
2303
|
+
}
|
|
2304
|
+
const dx = PADDING4 - tailX;
|
|
2305
|
+
const dy = PADDING4 - minY;
|
|
2306
|
+
let width = headLeft + headW - tailX + PADDING4 * 2;
|
|
2307
|
+
let height = maxY - minY + PADDING4 * 2;
|
|
2308
|
+
const spineY = dy;
|
|
2309
|
+
const centeredYs = (cy, n) => Array.from({ length: n }, (_, i) => cy - (n - 1) * lineH / 2 + i * lineH + fontSize * 0.32);
|
|
2310
|
+
const bandBaselines = (e, n, up) => Array.from(
|
|
2311
|
+
{ length: n },
|
|
2312
|
+
(_, k) => up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH
|
|
2313
|
+
);
|
|
2314
|
+
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="${round4(x)}" y="${round4(ys[i])}">${xmlEscape(line)}</tspan>`).join("") + `</text>`;
|
|
2315
|
+
const parts = [];
|
|
2316
|
+
{
|
|
2317
|
+
const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];
|
|
2318
|
+
if (arrows) body.push(arrowHead2(headLeft + dx, spineY, 1, 0, SPINE_OP));
|
|
2319
|
+
parts.push(`<g data-edge-id="spine"><title>${xmlEscape(labels.ariaLabel)}</title>${body.join("")}</g>`);
|
|
2320
|
+
}
|
|
2321
|
+
for (const bone of bones) {
|
|
2322
|
+
const sgn = bone.up ? -1 : 1;
|
|
2323
|
+
const ax = bone.ax + dx;
|
|
2324
|
+
const tipX = ax - bone.B / TAN60;
|
|
2325
|
+
const body = [lineEl(tipX, spineY + sgn * bone.B, ax, spineY, BONE_W, BONE_OP)];
|
|
2326
|
+
if (arrows) body.push(arrowHead2(ax, spineY, COS60, -sgn * SIN60, BONE_OP));
|
|
2327
|
+
const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;
|
|
2328
|
+
body.push(
|
|
2329
|
+
`<rect x="${round4(tipX - bone.boxW / 2)}" y="${round4(boxTop)}" width="${round4(bone.boxW)}" height="${round4(bone.boxH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="1.5"/>`
|
|
2330
|
+
);
|
|
2331
|
+
body.push(textBlock("middle", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));
|
|
2332
|
+
parts.push(
|
|
2333
|
+
`<g data-node-id="b${bone.category.id}"><title>${xmlEscape(bone.category.title ?? bone.category.label)}</title>${body.join("")}</g>`
|
|
2334
|
+
);
|
|
2335
|
+
for (const band of bone.bands) {
|
|
2336
|
+
const ty = spineY + sgn * band.offset;
|
|
2337
|
+
const bx = ax + band.bx;
|
|
2338
|
+
const freeEnd = ax + band.freeEnd;
|
|
2339
|
+
const cbody = [lineEl(freeEnd, ty, bx, ty, TWIG_W, TWIG_OP)];
|
|
2340
|
+
if (arrows) cbody.push(arrowHead2(bx, ty, 1, 0, TWIG_OP));
|
|
2341
|
+
cbody.push(textBlock("start", freeEnd + 2, bandBaselines(band.offset, band.lines.length, bone.up), band.lines));
|
|
2342
|
+
parts.push(
|
|
2343
|
+
`<g data-node-id="c${band.cause.id}"><title>${xmlEscape(band.cause.title ?? band.cause.label)}</title>${cbody.join("")}</g>`
|
|
2344
|
+
);
|
|
2345
|
+
for (const s of band.subs) {
|
|
2346
|
+
const sx = ax + s.sx;
|
|
2347
|
+
const ox = ax + s.outerX;
|
|
2348
|
+
const sbody = [lineEl(ox, spineY + sgn * (band.offset + sV), sx, ty, SUB_W, SUB_OP)];
|
|
2349
|
+
if (arrows) sbody.push(arrowHead2(sx, ty, COS60, -sgn * SIN60, SUB_OP));
|
|
2350
|
+
const baseline = bone.up ? spineY - (band.offset + sV + twigGap) : spineY + band.offset + sV + twigGap + ascent;
|
|
2351
|
+
sbody.push(textBlock("middle", ox, [baseline], [s.line]));
|
|
2352
|
+
parts.push(`<g data-node-id="s${s.sub.id}"><title>${xmlEscape(s.sub.label)}</title>${sbody.join("")}</g>`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
{
|
|
2357
|
+
const x = headLeft + dx;
|
|
2358
|
+
parts.push(
|
|
2359
|
+
`<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${round4(x)}" y="${round4(spineY - headH / 2)}" width="${round4(headW)}" height="${round4(headH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="2"/>` + textBlock("middle", x + headW / 2, centeredYs(spineY, effLines.length), effLines) + `</g>`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));
|
|
2363
|
+
if (opts.legend !== false && anySubs) {
|
|
2364
|
+
const entries = [
|
|
2365
|
+
{
|
|
2366
|
+
swatch: (x, y) => lineEl(x, y, x + LEGEND_SWATCH_W, y, TWIG_W, TWIG_OP),
|
|
2367
|
+
label: labels.cause
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
swatch: (x, y) => lineEl(x, y + 5, x + LEGEND_SWATCH_W, y - 5, SUB_W, SUB_OP),
|
|
2371
|
+
label: labels.subCause
|
|
2372
|
+
}
|
|
2373
|
+
];
|
|
2374
|
+
const block = legendBlock(entries, height);
|
|
2375
|
+
parts.push(block.svg);
|
|
2376
|
+
width = Math.max(width, block.width);
|
|
2377
|
+
height = block.height;
|
|
1403
2378
|
}
|
|
1404
2379
|
const w = Math.ceil(width);
|
|
1405
2380
|
const h = Math.ceil(height);
|
|
@@ -1407,15 +2382,36 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1407
2382
|
}
|
|
1408
2383
|
|
|
1409
2384
|
exports.CHAR_W = CHAR_W;
|
|
2385
|
+
exports.CODE_FONT = CODE_FONT;
|
|
1410
2386
|
exports.ECOMAP_LABELS_EN = ECOMAP_LABELS_EN;
|
|
1411
2387
|
exports.EDGE_FONT = EDGE_FONT;
|
|
1412
2388
|
exports.EDGE_STROKE = EDGE_STROKE;
|
|
2389
|
+
exports.FAULT_TREE_EVENT_KINDS = FAULT_TREE_EVENT_KINDS;
|
|
2390
|
+
exports.FAULT_TREE_SVG_LABELS_EN = FAULT_TREE_SVG_LABELS_EN;
|
|
2391
|
+
exports.FAULT_TREE_TITLE_LABELS_EN = FAULT_TREE_TITLE_LABELS_EN;
|
|
2392
|
+
exports.FISHBONE_LABELS_EN = FISHBONE_LABELS_EN;
|
|
1413
2393
|
exports.FONT_FAMILY = FONT_FAMILY;
|
|
2394
|
+
exports.FT_BUS_ID_BASE = FT_BUS_ID_BASE;
|
|
2395
|
+
exports.FT_CONDITION_ID_BASE = FT_CONDITION_ID_BASE;
|
|
2396
|
+
exports.FT_DROP_ID_BASE = FT_DROP_ID_BASE;
|
|
2397
|
+
exports.FT_LABEL_FONT = FT_LABEL_FONT;
|
|
2398
|
+
exports.FT_LABEL_LINE_H = FT_LABEL_LINE_H;
|
|
2399
|
+
exports.FT_RISER_ID_BASE = FT_RISER_ID_BASE;
|
|
2400
|
+
exports.FT_STEM_ID_BASE = FT_STEM_ID_BASE;
|
|
2401
|
+
exports.FaultTreeValidationError = FaultTreeValidationError;
|
|
2402
|
+
exports.FishboneValidationError = FishboneValidationError;
|
|
2403
|
+
exports.GATE_TYPES = GATE_TYPES;
|
|
1414
2404
|
exports.GENOGRAM_SVG_LABELS_EN = GENOGRAM_SVG_LABELS_EN;
|
|
1415
2405
|
exports.GENOGRAM_TITLE_LABELS_EN = GENOGRAM_TITLE_LABELS_EN;
|
|
1416
2406
|
exports.KINSHIP_EN = KINSHIP_EN;
|
|
1417
2407
|
exports.LABEL_FONT = LABEL_FONT;
|
|
2408
|
+
exports.LABEL_GAP = LABEL_GAP;
|
|
1418
2409
|
exports.LABEL_LINE_H = LABEL_LINE_H;
|
|
2410
|
+
exports.LEGEND_FONT = LEGEND_FONT;
|
|
2411
|
+
exports.LEGEND_GAP = LEGEND_GAP;
|
|
2412
|
+
exports.LEGEND_PAD = LEGEND_PAD;
|
|
2413
|
+
exports.LEGEND_ROW_H = LEGEND_ROW_H;
|
|
2414
|
+
exports.LEGEND_SWATCH_W = LEGEND_SWATCH_W;
|
|
1419
2415
|
exports.NODE_SIZE = NODE_SIZE;
|
|
1420
2416
|
exports.PARENT_REL_ID_BASE = PARENT_REL_ID_BASE;
|
|
1421
2417
|
exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
|
|
@@ -1425,17 +2421,25 @@ exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
|
|
|
1425
2421
|
exports.UNION_STATUSES = UNION_STATUSES;
|
|
1426
2422
|
exports.clampLabel = clampLabel;
|
|
1427
2423
|
exports.classifyRelationshipType = classifyRelationshipType;
|
|
2424
|
+
exports.computeFaultTreeLayout = computeFaultTreeLayout;
|
|
1428
2425
|
exports.computeGenogramLayout = computeGenogramLayout;
|
|
1429
2426
|
exports.ecomapSvg = ecomapSvg;
|
|
1430
2427
|
exports.estimateTextWidth = estimateTextWidth;
|
|
2428
|
+
exports.faultTreeIssues = faultTreeIssues;
|
|
2429
|
+
exports.faultTreeLayoutSvg = faultTreeLayoutSvg;
|
|
2430
|
+
exports.faultTreeSvg = faultTreeSvg;
|
|
2431
|
+
exports.fishboneSvg = fishboneSvg;
|
|
1431
2432
|
exports.genogramLayoutSvg = genogramLayoutSvg;
|
|
1432
2433
|
exports.genogramSvg = genogramSvg;
|
|
1433
2434
|
exports.latestUnionPerPair = latestUnionPerPair;
|
|
2435
|
+
exports.legendBlock = legendBlock;
|
|
1434
2436
|
exports.normalizeText = normalizeText;
|
|
1435
2437
|
exports.pathData = pathData;
|
|
1436
2438
|
exports.qualityLineStyle = qualityLineStyle;
|
|
1437
2439
|
exports.relationshipTypeTokens = relationshipTypeTokens;
|
|
2440
|
+
exports.validateFaultTree = validateFaultTree;
|
|
1438
2441
|
exports.wrapLabel = wrapLabel;
|
|
2442
|
+
exports.wrapLabelBalanced = wrapLabelBalanced;
|
|
1439
2443
|
exports.xmlEscape = xmlEscape;
|
|
1440
2444
|
//# sourceMappingURL=index.cjs.map
|
|
1441
2445
|
//# sourceMappingURL=index.cjs.map
|