compasso 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/chunk-5RRRE2GF.js +1125 -0
  4. package/dist/chunk-5RRRE2GF.js.map +1 -0
  5. package/dist/chunk-E456YKAJ.js +86 -0
  6. package/dist/chunk-E456YKAJ.js.map +1 -0
  7. package/dist/chunk-L5CYESBI.js +208 -0
  8. package/dist/chunk-L5CYESBI.js.map +1 -0
  9. package/dist/core/index.cjs +98 -0
  10. package/dist/core/index.cjs.map +1 -0
  11. package/dist/core/index.d.cts +36 -0
  12. package/dist/core/index.d.ts +36 -0
  13. package/dist/core/index.js +3 -0
  14. package/dist/core/index.js.map +1 -0
  15. package/dist/ecomap/index.cjs +287 -0
  16. package/dist/ecomap/index.cjs.map +1 -0
  17. package/dist/ecomap/index.d.cts +53 -0
  18. package/dist/ecomap/index.d.ts +53 -0
  19. package/dist/ecomap/index.js +4 -0
  20. package/dist/ecomap/index.js.map +1 -0
  21. package/dist/genogram/index.cjs +1222 -0
  22. package/dist/genogram/index.cjs.map +1 -0
  23. package/dist/genogram/index.d.cts +149 -0
  24. package/dist/genogram/index.d.ts +149 -0
  25. package/dist/genogram/index.js +4 -0
  26. package/dist/genogram/index.js.map +1 -0
  27. package/dist/index.cjs +1441 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +5 -0
  30. package/dist/index.d.ts +5 -0
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/kinship-BARO5-qz.d.cts +115 -0
  34. package/dist/kinship-Bkf87Jhu.d.ts +115 -0
  35. package/dist/locales/pt-br.cjs +123 -0
  36. package/dist/locales/pt-br.cjs.map +1 -0
  37. package/dist/locales/pt-br.d.cts +11 -0
  38. package/dist/locales/pt-br.d.ts +11 -0
  39. package/dist/locales/pt-br.js +117 -0
  40. package/dist/locales/pt-br.js.map +1 -0
  41. package/dist/stroke-MQ427drt.d.cts +35 -0
  42. package/dist/stroke-MQ427drt.d.ts +35 -0
  43. package/package.json +72 -0
@@ -0,0 +1,1222 @@
1
+ 'use strict';
2
+
3
+ // src/genogram/types.ts
4
+ var UNION_STATUSES = [
5
+ "married",
6
+ "cohabiting",
7
+ "dating",
8
+ "separated",
9
+ "divorced",
10
+ "coparental",
11
+ "unknown"
12
+ ];
13
+
14
+ // src/genogram/labels.ts
15
+ var GENOGRAM_TITLE_LABELS_EN = {
16
+ unionStatus: {
17
+ married: "married",
18
+ cohabiting: "cohabiting",
19
+ dating: "dating",
20
+ separated: "separated",
21
+ divorced: "divorced",
22
+ coparental: "co-parents (never a couple)",
23
+ unknown: "union (type not stated)"
24
+ },
25
+ parentage: "parent of"
26
+ };
27
+ var GENOGRAM_SVG_LABELS_EN = {
28
+ shapes: {
29
+ square: "Man",
30
+ circle: "Woman",
31
+ diamond: "Sex not stated"
32
+ },
33
+ deceased: "Deceased",
34
+ bondStyles: {
35
+ close: "Close",
36
+ distant: "Distant",
37
+ conflict: "Conflictual",
38
+ cutoff: "Cut off (no contact)"
39
+ },
40
+ isolated: "No recorded ancestry",
41
+ ariaLabel: "Family map (genogram)"
42
+ };
43
+
44
+ // src/core/xml.ts
45
+ function xmlEscape(text) {
46
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
47
+ }
48
+
49
+ // src/core/text.ts
50
+ var CHAR_W = 0.6;
51
+ function estimateTextWidth(text, fontPx) {
52
+ return text.length * fontPx * CHAR_W;
53
+ }
54
+ function wrapLabel(label, perLine) {
55
+ if (label.length <= perLine) return [label];
56
+ const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
57
+ let line1 = "";
58
+ let line2 = "";
59
+ for (const word of label.split(/\s+/)) {
60
+ if (line2 === "" && (line1 === "" || (line1 + " " + word).length <= perLine)) {
61
+ line1 = line1 === "" ? word : `${line1} ${word}`;
62
+ } else {
63
+ line2 = line2 === "" ? word : `${line2} ${word}`;
64
+ }
65
+ }
66
+ return line2 === "" ? [cap(line1)] : [cap(line1), cap(line2)];
67
+ }
68
+ var FONT_FAMILY = "Helvetica, Arial, sans-serif";
69
+
70
+ // src/core/stroke.ts
71
+ var EDGE_STROKE = {
72
+ plain: { width: 1.5, dash: null, opacity: 0.6 },
73
+ close: { width: 3, dash: null, opacity: 0.85 },
74
+ distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },
75
+ conflict: { width: 2, dash: [2, 2], opacity: 0.75 },
76
+ cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 }
77
+ };
78
+ var QUALITY_LEXICON_EN = {
79
+ buckets: [
80
+ {
81
+ style: "close",
82
+ needles: ["close", "warm", "support", "lov", "affection", "caring", "tight", "harmon", "healthy"]
83
+ },
84
+ {
85
+ style: "distant",
86
+ needles: ["distant", "detach", "absent", "cold", "drift"]
87
+ },
88
+ {
89
+ style: "conflict",
90
+ needles: ["conflict", "fight", "tens", "difficult", "hostil", "violen", "abus", "aggress", "complicat", "toxic", "argu"]
91
+ },
92
+ {
93
+ style: "cutoff",
94
+ needles: ["estrang", "cut off", "cutoff", "no contact", "broken off", "sever"]
95
+ }
96
+ ],
97
+ negations: ["not", "never", "no longer", "hardly"]
98
+ };
99
+ function normalizeText(text) {
100
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
101
+ }
102
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
+ function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
104
+ if (quality === null) return "plain";
105
+ const haystack = normalizeText(quality);
106
+ if (haystack.trim() === "") return "plain";
107
+ if (lexicon.negations.length > 0) {
108
+ const negation = new RegExp(`\\b(${lexicon.negations.map(escapeRegExp).join("|")})\\b`);
109
+ if (negation.test(haystack)) return "plain";
110
+ }
111
+ const matched = [];
112
+ for (const { style, needles } of lexicon.buckets) {
113
+ if (needles.some((n) => haystack.includes(n))) matched.push(style);
114
+ }
115
+ return matched.length === 1 ? matched[0] : "plain";
116
+ }
117
+
118
+ // src/genogram/kinship.ts
119
+ var KINSHIP_EN = {
120
+ derived: /* @__PURE__ */ new Set([
121
+ "brother",
122
+ "brothers",
123
+ "sister",
124
+ "sisters",
125
+ "sibling",
126
+ "siblings",
127
+ "grandmother",
128
+ "grandfather",
129
+ "grandma",
130
+ "grandpa",
131
+ "granny",
132
+ "grandad",
133
+ "granddad",
134
+ "grandparent",
135
+ "grandparents",
136
+ "grandson",
137
+ "granddaughter",
138
+ "grandchild",
139
+ "grandchildren",
140
+ "uncle",
141
+ "uncles",
142
+ "aunt",
143
+ "aunts",
144
+ "auntie",
145
+ "cousin",
146
+ "cousins",
147
+ "nephew",
148
+ "nephews",
149
+ "niece",
150
+ "nieces"
151
+ ]),
152
+ parentage: /* @__PURE__ */ new Set([
153
+ "mother",
154
+ "mothers",
155
+ "mom",
156
+ "mum",
157
+ "father",
158
+ "fathers",
159
+ "dad",
160
+ "parent",
161
+ "parents",
162
+ "son",
163
+ "sons",
164
+ "daughter",
165
+ "daughters"
166
+ ]),
167
+ childWords: /* @__PURE__ */ new Set(["son", "sons", "daughter", "daughters"])
168
+ };
169
+ function relationshipTypeTokens(type) {
170
+ return normalizeText(type).split(/[^a-z0-9]+/).filter((t) => t !== "");
171
+ }
172
+ function classifyRelationshipType(type, kinship = KINSHIP_EN) {
173
+ const toks = relationshipTypeTokens(type);
174
+ if (toks.some((t) => kinship.derived.has(t))) return "derived";
175
+ if (toks.some((t) => kinship.parentage.has(t))) return "parentage";
176
+ return "bond";
177
+ }
178
+
179
+ // src/genogram/layout.ts
180
+ var NODE_SIZE = 56;
181
+ var PADDING = 32;
182
+ var COL_PAD = 30;
183
+ var LABEL_GAP_TOP = 8;
184
+ var CORRIDOR_BASE = 24;
185
+ var LANE_H = 8;
186
+ var UNION_STAGGER = 12;
187
+ var STUB_X_OFF = 14;
188
+ var ISOLATED_GUTTER_EXTRA = 2 * COL_PAD;
189
+ var LABEL_FONT = 12;
190
+ var LABEL_LINE_H = 14;
191
+ var EDGE_FONT = 11;
192
+ var UNION_REL_ID_BASE = 1e6;
193
+ var PARENT_REL_ID_BASE = 2e6;
194
+ var PROMOTED_REL_ID_BASE = 3e6;
195
+ function shapeForSex(sex) {
196
+ if (sex === "male") return "square";
197
+ if (sex === "female") return "circle";
198
+ return "diamond";
199
+ }
200
+ function wrapNodeLabel(displayLabel) {
201
+ const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));
202
+ return wrapLabel(displayLabel, perLine);
203
+ }
204
+ function clampLabel2(label, maxChars) {
205
+ if (maxChars === void 0 || label.length <= maxChars) return label;
206
+ return label.slice(0, Math.max(1, maxChars - 1)) + "\u2026";
207
+ }
208
+ var STUB_OFFSETS = [9, -9, 18, -18, 26, -26];
209
+ var ARRIVAL_HALF = NODE_SIZE / 2 - 8;
210
+ function stubOffset(slot) {
211
+ return STUB_OFFSETS[Math.min(slot, STUB_OFFSETS.length - 1)];
212
+ }
213
+ function arrivalOffset(slot, total) {
214
+ if (slot === 0 || total <= 1) return 0;
215
+ const step = Math.min(12, ARRIVAL_HALF / Math.max(1, Math.ceil((total - 1) / 2)));
216
+ const sign = slot % 2 === 1 ? -1 : 1;
217
+ return sign * Math.ceil(slot / 2) * step;
218
+ }
219
+ function allocateLanes(items) {
220
+ const lanes = [];
221
+ for (const it of items) {
222
+ let chosen = -1;
223
+ for (let l = 0; l < lanes.length; l++) {
224
+ if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
225
+ chosen = l;
226
+ break;
227
+ }
228
+ }
229
+ if (chosen === -1) {
230
+ chosen = lanes.length;
231
+ lanes.push([]);
232
+ }
233
+ lanes[chosen].push({ lo: it.lo, hi: it.hi });
234
+ it.set(chosen);
235
+ }
236
+ return lanes.length;
237
+ }
238
+ function computeGenogramLayout(people, unions, parentLinks, relationships, opts = {}) {
239
+ const kinship = opts.kinship ?? KINSHIP_EN;
240
+ const titleLabels = opts.titleLabels ?? GENOGRAM_TITLE_LABELS_EN;
241
+ if (people.length === 0) {
242
+ return { width: PADDING * 2, height: PADDING * 2, nodes: [], elements: [], isolatedPersonIds: [] };
243
+ }
244
+ const ids = new Set(people.map((p) => p.id));
245
+ const pairKey = (a, b) => `${Math.min(a, b)}|${Math.max(a, b)}`;
246
+ const genById = new Map(people.map((p) => [p.id, p.generation]));
247
+ const validUnions = unions.filter((u) => ids.has(u.personAId) && ids.has(u.personBId) && u.personAId !== u.personBId).sort((a, b) => a.id - b.id);
248
+ const coupleUnions = validUnions.filter((u) => u.status !== "coparental");
249
+ const validLinks = parentLinks.filter((l) => ids.has(l.parentId) && ids.has(l.childId) && l.parentId !== l.childId).sort((a, b) => a.id - b.id);
250
+ const validRels = relationships.filter((r) => ids.has(r.fromPersonId) && ids.has(r.toPersonId) && r.fromPersonId !== r.toPersonId).sort((a, b) => a.id - b.id);
251
+ const relClass = new Map(
252
+ validRels.map((r) => [r.id, classifyRelationshipType(r.type, kinship)])
253
+ );
254
+ const bondRels = validRels.filter((r) => relClass.get(r.id) === "bond");
255
+ const parentageRels = validRels.filter((r) => relClass.get(r.id) === "parentage");
256
+ const realLinks = validLinks.map((l) => ({
257
+ parentId: l.parentId,
258
+ childId: l.childId,
259
+ quality: l.quality,
260
+ edgeId: PARENT_REL_ID_BASE + l.id
261
+ }));
262
+ const declaredPairs = new Set(validLinks.map((l) => pairKey(l.parentId, l.childId)));
263
+ const promotedByPair = /* @__PURE__ */ new Map();
264
+ for (const r of parentageRels) {
265
+ const key = pairKey(r.fromPersonId, r.toPersonId);
266
+ if (declaredPairs.has(key)) continue;
267
+ if (promotedByPair.has(key)) continue;
268
+ const ga = genById.get(r.fromPersonId) ?? null;
269
+ const gb = genById.get(r.toPersonId) ?? null;
270
+ let parentId;
271
+ let childId;
272
+ if (ga !== null && gb !== null && ga !== gb) {
273
+ [parentId, childId] = ga < gb ? [r.fromPersonId, r.toPersonId] : [r.toPersonId, r.fromPersonId];
274
+ } else {
275
+ const fromIsChild = relationshipTypeTokens(r.type).some((t) => kinship.childWords.has(t));
276
+ [parentId, childId] = fromIsChild ? [r.toPersonId, r.fromPersonId] : [r.fromPersonId, r.toPersonId];
277
+ }
278
+ if (parentId === childId) continue;
279
+ promotedByPair.set(key, { parentId, childId, quality: r.quality, edgeId: PROMOTED_REL_ID_BASE + r.id });
280
+ }
281
+ const allLinks = [...realLinks, ...promotedByPair.values()].sort((a, b) => a.edgeId - b.edgeId);
282
+ const coupleByPair = /* @__PURE__ */ new Map();
283
+ for (const u of coupleUnions) coupleByPair.set(pairKey(u.personAId, u.personBId), u);
284
+ const parentsOf = /* @__PURE__ */ new Map();
285
+ const linkOf = /* @__PURE__ */ new Map();
286
+ for (const l of allLinks) {
287
+ const arr = parentsOf.get(l.childId) ?? [];
288
+ if (!arr.includes(l.parentId)) arr.push(l.parentId);
289
+ parentsOf.set(l.childId, arr);
290
+ linkOf.set(`${l.parentId}>${l.childId}`, l);
291
+ }
292
+ const hasTie = /* @__PURE__ */ new Set();
293
+ for (const u of validUnions) {
294
+ hasTie.add(u.personAId);
295
+ hasTie.add(u.personBId);
296
+ }
297
+ for (const l of allLinks) {
298
+ hasTie.add(l.parentId);
299
+ hasTie.add(l.childId);
300
+ }
301
+ for (const r of bondRels) {
302
+ hasTie.add(r.fromPersonId);
303
+ hasTie.add(r.toPersonId);
304
+ }
305
+ const isIsolated = (id) => !hasTie.has(id);
306
+ const byGen = /* @__PURE__ */ new Map();
307
+ for (const p of people) {
308
+ const bucket = byGen.get(p.generation);
309
+ if (bucket === void 0) byGen.set(p.generation, [p]);
310
+ else bucket.push(p);
311
+ }
312
+ const genKeys = [...byGen.keys()].sort((a, b) => {
313
+ if (a === null) return 1;
314
+ if (b === null) return -1;
315
+ return a - b;
316
+ });
317
+ const rowCount = genKeys.length;
318
+ const rowOfPerson = /* @__PURE__ */ new Map();
319
+ genKeys.forEach((g, r) => byGen.get(g).forEach((p) => rowOfPerson.set(p.id, r)));
320
+ const rowBlocks = genKeys.map((g, r) => {
321
+ const members = byGen.get(g).map((p) => p.id);
322
+ const adj = /* @__PURE__ */ new Map();
323
+ for (const id of members) adj.set(id, []);
324
+ for (const u of coupleUnions) {
325
+ if (rowOfPerson.get(u.personAId) === r && rowOfPerson.get(u.personBId) === r) {
326
+ adj.get(u.personAId).push(u.personBId);
327
+ adj.get(u.personBId).push(u.personAId);
328
+ }
329
+ }
330
+ for (const id of members) adj.get(id).sort((a, b) => a - b);
331
+ const seen = /* @__PURE__ */ new Set();
332
+ const blocks = [];
333
+ for (const start of [...members].sort((a, b) => a - b)) {
334
+ if (seen.has(start)) continue;
335
+ const comp = [];
336
+ const stack = [start];
337
+ seen.add(start);
338
+ while (stack.length > 0) {
339
+ const n = stack.pop();
340
+ comp.push(n);
341
+ for (const m of adj.get(n)) if (!seen.has(m)) {
342
+ seen.add(m);
343
+ stack.push(m);
344
+ }
345
+ }
346
+ blocks.push(linearizeBlock(comp, adj));
347
+ }
348
+ return blocks;
349
+ });
350
+ const flattened = () => {
351
+ const pos = /* @__PURE__ */ new Map();
352
+ for (const blocks of rowBlocks) {
353
+ let c = 0;
354
+ for (const b of blocks) for (const id of b) pos.set(id, c++);
355
+ }
356
+ return pos;
357
+ };
358
+ const structuralNeighbors = (id) => {
359
+ const out = [];
360
+ for (const u of coupleUnions) {
361
+ if (u.personAId === id) out.push({ id: u.personBId, w: 1 });
362
+ else if (u.personBId === id) out.push({ id: u.personAId, w: 1 });
363
+ }
364
+ for (const l of allLinks) {
365
+ if (l.childId === id) out.push({ id: l.parentId, w: 1 });
366
+ else if (l.parentId === id) out.push({ id: l.childId, w: 1 });
367
+ }
368
+ for (const rl of bondRels) {
369
+ if (rl.fromPersonId === id) out.push({ id: rl.toPersonId, w: 0.5 });
370
+ else if (rl.toPersonId === id) out.push({ id: rl.fromPersonId, w: 0.5 });
371
+ }
372
+ return out.map((o) => o.id);
373
+ };
374
+ const neighborWeight = (id, other) => {
375
+ let w = 0;
376
+ for (const u of coupleUnions) if (u.personAId === id && u.personBId === other || u.personBId === id && u.personAId === other) w = Math.max(w, 1);
377
+ for (const l of allLinks) if (l.childId === id && l.parentId === other || l.parentId === id && l.childId === other) w = Math.max(w, 1);
378
+ for (const rl of bondRels) if (rl.fromPersonId === id && rl.toPersonId === other || rl.toPersonId === id && rl.fromPersonId === other) w = Math.max(w, 0.5);
379
+ return w;
380
+ };
381
+ const sweep = (r, ref) => {
382
+ const pos = flattened();
383
+ const blocks = rowBlocks[r];
384
+ const bcOf = (b) => {
385
+ let sum = 0;
386
+ let wsum = 0;
387
+ for (const id of b) {
388
+ for (const nb of structuralNeighbors(id)) {
389
+ if (rowOfPerson.get(nb) !== ref) continue;
390
+ const w = neighborWeight(id, nb);
391
+ sum += (pos.get(nb) ?? 0) * w;
392
+ wsum += w;
393
+ }
394
+ }
395
+ return wsum === 0 ? Number.POSITIVE_INFINITY : sum / wsum;
396
+ };
397
+ const keyed = blocks.map((b) => ({ b, bc: bcOf(b), minId: Math.min(...b) }));
398
+ keyed.sort((x, y) => x.bc !== y.bc ? x.bc - y.bc : x.minId - y.minId);
399
+ rowBlocks[r] = keyed.map((k) => k.b);
400
+ };
401
+ if (rowCount >= 2) {
402
+ for (let r = 1; r < rowCount; r++) sweep(r, r - 1);
403
+ for (let r = rowCount - 2; r >= 0; r--) sweep(r, r + 1);
404
+ }
405
+ for (let r = 0; r < rowCount; r++) {
406
+ const blocks = rowBlocks[r];
407
+ const connected = blocks.filter((b) => !(b.length === 1 && isIsolated(b[0])));
408
+ const isolated = blocks.filter((b) => b.length === 1 && isIsolated(b[0])).sort((a, b) => a[0] - b[0]);
409
+ rowBlocks[r] = [...connected, ...isolated];
410
+ }
411
+ const colOf = /* @__PURE__ */ new Map();
412
+ let colCount = 0;
413
+ for (let r = 0; r < rowCount; r++) {
414
+ let c = 0;
415
+ for (const b of rowBlocks[r]) for (const id of b) colOf.set(id, c++);
416
+ colCount = Math.max(colCount, c);
417
+ }
418
+ const colOrThrow = (id) => colOf.get(id);
419
+ let isolatedTailCol = colCount;
420
+ {
421
+ const peopleAtCol = Array.from({ length: colCount }, () => []);
422
+ for (const p of people) peopleAtCol[colOrThrow(p.id)].push(p.id);
423
+ for (let c = colCount - 1; c >= 1; c--) {
424
+ const all = peopleAtCol[c];
425
+ if (all.length > 0 && all.every((id) => isIsolated(id))) isolatedTailCol = c;
426
+ else break;
427
+ }
428
+ }
429
+ const measured = /* @__PURE__ */ new Map();
430
+ for (const p of people) {
431
+ const lines = wrapNodeLabel(clampLabel2(p.label, opts.maxLabelChars));
432
+ const contentW = Math.max(NODE_SIZE, lines.reduce((m, l) => Math.max(m, estimateTextWidth(l, LABEL_FONT)), 0));
433
+ measured.set(p.id, { person: p, lines, contentW });
434
+ }
435
+ const colWidth = Array.from({ length: colCount }, () => NODE_SIZE);
436
+ for (const p of people) {
437
+ const c = colOrThrow(p.id);
438
+ colWidth[c] = Math.max(colWidth[c], measured.get(p.id).contentW);
439
+ }
440
+ const rowMaxLines = Array.from({ length: rowCount }, () => 1);
441
+ for (const p of people) {
442
+ const r = rowOfPerson.get(p.id);
443
+ rowMaxLines[r] = Math.max(rowMaxLines[r], measured.get(p.id).lines.length);
444
+ }
445
+ const planned = [];
446
+ const gutterReqs = [];
447
+ const corridorReqs = [];
448
+ const stubCount = /* @__PURE__ */ new Map();
449
+ const arrivalCount = /* @__PURE__ */ new Map();
450
+ const newStub = (personId, side) => {
451
+ const key = `${personId}:${side}`;
452
+ const slot = stubCount.get(key) ?? 0;
453
+ stubCount.set(key, slot + 1);
454
+ return { personId, side, slot };
455
+ };
456
+ const newArrival = (personId) => {
457
+ const slot = arrivalCount.get(personId) ?? 0;
458
+ arrivalCount.set(personId, slot + 1);
459
+ return { personId, slot };
460
+ };
461
+ const departSide = (srcId, dstId) => {
462
+ const sc = colOrThrow(srcId);
463
+ const dc = colOrThrow(dstId);
464
+ if (dc > sc) return 1;
465
+ if (dc < sc) return -1;
466
+ return sc < colCount - 1 ? 1 : -1;
467
+ };
468
+ const gutterFor = (srcCol, side) => side === 1 ? { gutter: srcCol, gSide: -1 } : { gutter: srcCol - 1, gSide: 1 };
469
+ const planConnector = (srcId, dstId, arrivalAtCenter) => {
470
+ const srcRow = rowOfPerson.get(srcId);
471
+ const dstRow = rowOfPerson.get(dstId);
472
+ const [upperId, lowerId] = srcRow <= dstRow ? [srcId, dstId] : [dstId, srcId];
473
+ const upperRow = rowOfPerson.get(upperId);
474
+ const lowerRow = rowOfPerson.get(lowerId);
475
+ const side = departSide(upperId, lowerId);
476
+ const { gutter, gSide } = gutterFor(colOrThrow(upperId), side);
477
+ const stub = newStub(upperId, side);
478
+ const corridor = Math.max(0, Math.min(rowCount - 1, lowerRow - 1));
479
+ const gReq = {
480
+ gutter,
481
+ side: gSide,
482
+ rowLo: Math.min(upperRow, corridor + 0.5),
483
+ rowHi: Math.max(upperRow, corridor + 0.5),
484
+ lane: 0
485
+ };
486
+ gutterReqs.push(gReq);
487
+ const arrival = newArrival(lowerId);
488
+ const arrivalX = () => geo.cx(colOrThrow(lowerId)) + (arrival ? arrivalOffset(arrival.slot, arrivalCount.get(lowerId) ?? 1) : 0);
489
+ const cReq = {
490
+ corridor,
491
+ xRange: () => {
492
+ const gx = geo.gutterLaneX(gReq);
493
+ const ax = arrivalX();
494
+ return [Math.min(gx, ax), Math.max(gx, ax)];
495
+ },
496
+ lane: 0
497
+ };
498
+ corridorReqs.push(cReq);
499
+ return () => {
500
+ const sy = geo.cy(upperRow) + stubOffset(stub.slot);
501
+ const sEdge = side === 1 ? geo.glyphRight(upperId) : geo.glyphLeft(upperId);
502
+ const gx = geo.gutterLaneX(gReq);
503
+ const cyCorr = geo.corridorLaneY(cReq);
504
+ const ax = arrivalX();
505
+ const topY = geo.cy(lowerRow) - NODE_SIZE / 2;
506
+ return [
507
+ { x: sEdge, y: sy },
508
+ { x: gx, y: sy },
509
+ { x: gx, y: cyCorr },
510
+ { x: ax, y: cyCorr },
511
+ { x: ax, y: topY }
512
+ ];
513
+ };
514
+ };
515
+ const planUConnector = (aId, bId) => {
516
+ const row = rowOfPerson.get(aId);
517
+ const [leftId, rightId] = colOrThrow(aId) <= colOrThrow(bId) ? [aId, bId] : [bId, aId];
518
+ const stubL = newStub(leftId, 1);
519
+ const stubR = newStub(rightId, -1);
520
+ const gL = gutterFor(colOrThrow(leftId), 1);
521
+ const gR = gutterFor(colOrThrow(rightId), -1);
522
+ const gReqL = { gutter: gL.gutter, side: gL.gSide, rowLo: row, rowHi: row + 0.5, lane: 0 };
523
+ const gReqR = { gutter: gR.gutter, side: gR.gSide, rowLo: row, rowHi: row + 0.5, lane: 0 };
524
+ gutterReqs.push(gReqL, gReqR);
525
+ const cReq = {
526
+ corridor: row,
527
+ xRange: () => {
528
+ const xL = geo.gutterLaneX(gReqL);
529
+ const xR = geo.gutterLaneX(gReqR);
530
+ return [Math.min(xL, xR), Math.max(xL, xR)];
531
+ },
532
+ lane: 0
533
+ };
534
+ corridorReqs.push(cReq);
535
+ return () => {
536
+ const yL = geo.cy(row) + stubOffset(stubL.slot);
537
+ const yR = geo.cy(row) + stubOffset(stubR.slot);
538
+ const xL = geo.gutterLaneX(gReqL);
539
+ const xR = geo.gutterLaneX(gReqR);
540
+ const cyCorr = geo.corridorLaneY(cReq);
541
+ return [
542
+ { x: geo.glyphRight(leftId), y: yL },
543
+ { x: xL, y: yL },
544
+ { x: xL, y: cyCorr },
545
+ { x: xR, y: cyCorr },
546
+ { x: xR, y: yR },
547
+ { x: geo.glyphLeft(rightId), y: yR }
548
+ ];
549
+ };
550
+ };
551
+ const unionTitle = (u) => u.quality !== null ? `${titleLabels.unionStatus[u.status]} \xB7 ${u.quality}` : titleLabels.unionStatus[u.status];
552
+ const linkTitle = (l) => l.quality !== null ? `${titleLabels.parentage} \xB7 ${l.quality}` : titleLabels.parentage;
553
+ const unionIsBar = /* @__PURE__ */ new Map();
554
+ const barUnions = coupleUnions.filter(
555
+ (u) => rowOfPerson.get(u.personAId) === rowOfPerson.get(u.personBId) && Math.abs(colOrThrow(u.personAId) - colOrThrow(u.personBId)) === 1
556
+ );
557
+ const unionDipLevel = /* @__PURE__ */ new Map();
558
+ const rowDipLevels = Array.from({ length: rowCount }, () => 0);
559
+ {
560
+ const byRow = /* @__PURE__ */ new Map();
561
+ for (const u of barUnions) {
562
+ const r = rowOfPerson.get(u.personAId);
563
+ (byRow.get(r) ?? byRow.set(r, []).get(r)).push(u);
564
+ }
565
+ for (const [r, rowUnions] of byRow) {
566
+ const unionsOfPerson = /* @__PURE__ */ new Map();
567
+ for (const u of rowUnions) {
568
+ (unionsOfPerson.get(u.personAId) ?? unionsOfPerson.set(u.personAId, []).get(u.personAId)).push(u);
569
+ (unionsOfPerson.get(u.personBId) ?? unionsOfPerson.set(u.personBId, []).get(u.personBId)).push(u);
570
+ }
571
+ const seen = /* @__PURE__ */ new Set();
572
+ for (const start of [...rowUnions].sort((a, b) => a.id - b.id)) {
573
+ if (seen.has(start.id)) continue;
574
+ const comp = [];
575
+ const stack = [start];
576
+ seen.add(start.id);
577
+ while (stack.length > 0) {
578
+ const u = stack.pop();
579
+ comp.push(u);
580
+ for (const pid of [u.personAId, u.personBId]) {
581
+ for (const nb of unionsOfPerson.get(pid) ?? []) {
582
+ if (!seen.has(nb.id)) {
583
+ seen.add(nb.id);
584
+ stack.push(nb);
585
+ }
586
+ }
587
+ }
588
+ }
589
+ comp.sort((a, b) => a.id - b.id);
590
+ comp.forEach((u, i) => unionDipLevel.set(u.id, i));
591
+ rowDipLevels[r] = Math.max(rowDipLevels[r], comp.length - 1);
592
+ }
593
+ }
594
+ }
595
+ for (const u of coupleUnions) {
596
+ const aRow = rowOfPerson.get(u.personAId);
597
+ const bRow = rowOfPerson.get(u.personBId);
598
+ const adjacent = aRow === bRow && Math.abs(colOrThrow(u.personAId) - colOrThrow(u.personBId)) === 1;
599
+ unionIsBar.set(u.id, adjacent);
600
+ const edgeId = UNION_REL_ID_BASE + u.id;
601
+ if (adjacent) {
602
+ const [leftId, rightId] = colOrThrow(u.personAId) < colOrThrow(u.personBId) ? [u.personAId, u.personBId] : [u.personBId, u.personAId];
603
+ const dipLevel = unionDipLevel.get(u.id) ?? 0;
604
+ planned.push({
605
+ kind: "union-bar",
606
+ relIds: [edgeId],
607
+ edgeId,
608
+ fromPersonId: u.personAId,
609
+ toPersonId: u.personBId,
610
+ titles: [unionTitle(u)],
611
+ lineStyle: "plain",
612
+ build: () => {
613
+ const cy = geo.cy(rowOfPerson.get(leftId));
614
+ if (dipLevel === 0) {
615
+ return [
616
+ { x: geo.glyphRight(leftId), y: cy },
617
+ { x: geo.glyphLeft(rightId), y: cy }
618
+ ];
619
+ }
620
+ const y = geo.unionDipY(rowOfPerson.get(leftId), dipLevel);
621
+ const bottom = cy + NODE_SIZE / 2;
622
+ const lx = geo.cx(colOrThrow(leftId)) + STUB_X_OFF;
623
+ const rx = geo.cx(colOrThrow(rightId)) - STUB_X_OFF;
624
+ return [
625
+ { x: lx, y: bottom },
626
+ { x: lx, y },
627
+ { x: rx, y },
628
+ { x: rx, y: bottom }
629
+ ];
630
+ }
631
+ });
632
+ } else {
633
+ const sameRow = aRow === bRow;
634
+ const build = sameRow ? planUConnector(u.personAId, u.personBId) : (() => {
635
+ const [up, lo] = aRow < bRow ? [u.personAId, u.personBId] : [u.personBId, u.personAId];
636
+ return planConnector(up, lo);
637
+ })();
638
+ planned.push({
639
+ kind: "union-elbow",
640
+ relIds: [edgeId],
641
+ edgeId,
642
+ fromPersonId: u.personAId,
643
+ toPersonId: u.personBId,
644
+ titles: [unionTitle(u)],
645
+ lineStyle: "plain",
646
+ build
647
+ });
648
+ }
649
+ }
650
+ const coupleChildrenByUnion = /* @__PURE__ */ new Map();
651
+ const loneLinks = [];
652
+ for (const childId of [...parentsOf.keys()].sort((a, b) => a - b)) {
653
+ const parents = [...parentsOf.get(childId)].sort((a, b) => a - b);
654
+ const couplePairUnions = parents.flatMap((p, i) => parents.slice(i + 1).map((q) => coupleByPair.get(pairKey(p, q)))).filter((u) => u !== void 0 && unionIsBar.get(u.id) === true);
655
+ if (couplePairUnions.length === 1) {
656
+ const u = couplePairUnions[0];
657
+ const arr = coupleChildrenByUnion.get(u.id) ?? [];
658
+ arr.push(childId);
659
+ coupleChildrenByUnion.set(u.id, arr);
660
+ const couplePeople = /* @__PURE__ */ new Set([u.personAId, u.personBId]);
661
+ for (const p of parents) if (!couplePeople.has(p)) loneLinks.push(linkOf.get(`${p}>${childId}`));
662
+ } else {
663
+ for (const p of parents) loneLinks.push(linkOf.get(`${p}>${childId}`));
664
+ }
665
+ }
666
+ for (const u of coupleUnions) {
667
+ const children = coupleChildrenByUnion.get(u.id);
668
+ if (children === void 0 || children.length === 0) continue;
669
+ const leftId = colOrThrow(u.personAId) < colOrThrow(u.personBId) ? u.personAId : u.personBId;
670
+ const interGutter = colOrThrow(leftId);
671
+ const parentRow = rowOfPerson.get(u.personAId);
672
+ const uDipLevel = unionDipLevel.get(u.id) ?? 0;
673
+ const spineStartY = () => uDipLevel === 0 ? geo.cy(parentRow) : geo.unionDipY(parentRow, uDipLevel);
674
+ const byChildRow = /* @__PURE__ */ new Map();
675
+ for (const c of children) {
676
+ const cr = rowOfPerson.get(c);
677
+ (byChildRow.get(cr) ?? byChildRow.set(cr, []).get(cr)).push(c);
678
+ }
679
+ for (const childRow of [...byChildRow.keys()].sort((a, b) => a - b)) {
680
+ const groupKids = byChildRow.get(childRow).sort((a, b) => colOrThrow(a) - colOrThrow(b));
681
+ const corridor = Math.max(0, Math.min(rowCount - 1, childRow - 1));
682
+ const sibSpan = () => [geo.gutterCenterX(interGutter), ...groupKids.map((c) => geo.cx(colOrThrow(c)))];
683
+ const sibReq = {
684
+ corridor,
685
+ xRange: () => {
686
+ const xs = sibSpan();
687
+ return [Math.min(...xs), Math.max(...xs)];
688
+ },
689
+ lane: 0
690
+ };
691
+ corridorReqs.push(sibReq);
692
+ const spineReq = {
693
+ gutter: interGutter,
694
+ side: 0,
695
+ rowLo: Math.min(parentRow, corridor + 0.5),
696
+ // ordered (FIX D-2): never inverted
697
+ rowHi: Math.max(parentRow, corridor + 0.5),
698
+ lane: 0
699
+ };
700
+ gutterReqs.push(spineReq);
701
+ const aId = (c) => linkOf.get(`${u.personAId}>${c}`).edgeId;
702
+ const childDrop = (c, parentId) => {
703
+ const link = linkOf.get(`${parentId}>${c}`);
704
+ const edgeId = link.edgeId;
705
+ const arr = newArrival(c);
706
+ planned.push({
707
+ kind: "descent",
708
+ relIds: [edgeId],
709
+ edgeId,
710
+ fromPersonId: null,
711
+ toPersonId: c,
712
+ titles: [linkTitle(link)],
713
+ lineStyle: "plain",
714
+ build: () => {
715
+ const y = geo.corridorLaneY(sibReq);
716
+ const x = geo.cx(colOrThrow(c)) + arrivalOffset(arr.slot, arrivalCount.get(c) ?? 1);
717
+ return [
718
+ { x, y },
719
+ { x, y: geo.cy(rowOfPerson.get(c)) - NODE_SIZE / 2 }
720
+ ];
721
+ }
722
+ });
723
+ };
724
+ if (groupKids.length === 1) {
725
+ const c = groupKids[0];
726
+ planned.push({
727
+ kind: "descent",
728
+ relIds: [aId(c)],
729
+ edgeId: aId(c),
730
+ fromPersonId: null,
731
+ toPersonId: null,
732
+ titles: [linkTitle(linkOf.get(`${u.personAId}>${c}`))],
733
+ lineStyle: "plain",
734
+ build: () => {
735
+ const sy = geo.corridorLaneY(sibReq);
736
+ const sx = geo.gutterCenterX(interGutter);
737
+ const cx = geo.cx(colOrThrow(c));
738
+ return [
739
+ { x: sx, y: spineStartY() },
740
+ { x: sx, y: sy },
741
+ { x: cx, y: sy }
742
+ ];
743
+ }
744
+ });
745
+ childDrop(c, u.personBId);
746
+ } else {
747
+ const aLinks = groupKids.map((c) => aId(c));
748
+ const aLinkTitle = (c) => linkTitle(linkOf.get(`${u.personAId}>${c}`));
749
+ planned.push({
750
+ kind: "sibling-bar",
751
+ relIds: [aLinks[0], ...aLinks.slice(2)],
752
+ edgeId: aLinks[0],
753
+ fromPersonId: null,
754
+ toPersonId: null,
755
+ // Carry the verbatim title of EVERY A-side link this bar represents, so no
756
+ // declared quality word is dropped from the drawn element (FIX C-2 / SPEC inv #4).
757
+ titles: [aLinkTitle(groupKids[0]), ...groupKids.slice(2).map(aLinkTitle)],
758
+ lineStyle: "plain",
759
+ build: () => {
760
+ const y = geo.corridorLaneY(sibReq);
761
+ const xs = sibSpan();
762
+ return [
763
+ { x: Math.min(...xs), y },
764
+ { x: Math.max(...xs), y }
765
+ ];
766
+ }
767
+ });
768
+ planned.push({
769
+ kind: "descent",
770
+ relIds: [aLinks[1]],
771
+ edgeId: aLinks[1],
772
+ fromPersonId: null,
773
+ toPersonId: null,
774
+ titles: [aLinkTitle(groupKids[1])],
775
+ // the A-side link this spine carries, verbatim
776
+ lineStyle: "plain",
777
+ build: () => {
778
+ const sy = geo.corridorLaneY(sibReq);
779
+ const x = geo.gutterCenterX(interGutter);
780
+ return [
781
+ { x, y: spineStartY() },
782
+ { x, y: sy }
783
+ ];
784
+ }
785
+ });
786
+ for (const c of groupKids) childDrop(c, u.personBId);
787
+ }
788
+ }
789
+ }
790
+ for (const l of [...loneLinks].sort((a, b) => a.edgeId - b.edgeId)) {
791
+ const edgeId = l.edgeId;
792
+ const pRow = rowOfPerson.get(l.parentId);
793
+ const cRow = rowOfPerson.get(l.childId);
794
+ const build = pRow === cRow ? planUConnector(l.parentId, l.childId) : planConnector(l.parentId, l.childId);
795
+ planned.push({
796
+ kind: "descent",
797
+ relIds: [edgeId],
798
+ edgeId,
799
+ fromPersonId: l.parentId,
800
+ toPersonId: l.childId,
801
+ titles: [linkTitle(l)],
802
+ lineStyle: "plain",
803
+ dotted: true,
804
+ build
805
+ });
806
+ }
807
+ const bondGroups = /* @__PURE__ */ new Map();
808
+ for (const r of bondRels) {
809
+ const style = qualityLineStyle(r.quality, opts.qualityLexicon);
810
+ const lo = Math.min(r.fromPersonId, r.toPersonId);
811
+ const hi = Math.max(r.fromPersonId, r.toPersonId);
812
+ const key = `${lo}|${hi}|${style}`;
813
+ const title = r.quality !== null ? `${r.type} \xB7 ${r.quality}` : r.type;
814
+ const g = bondGroups.get(key);
815
+ if (g === void 0) {
816
+ bondGroups.set(key, { relIds: [r.id], titles: [title], style, aId: r.fromPersonId, bId: r.toPersonId });
817
+ } else {
818
+ g.relIds.push(r.id);
819
+ g.titles.push(title);
820
+ }
821
+ }
822
+ const bondList = [...bondGroups.values()].sort((a, b) => Math.max(...a.relIds) - Math.max(...b.relIds));
823
+ for (const g of bondList) {
824
+ const aRow = rowOfPerson.get(g.aId);
825
+ const bRow = rowOfPerson.get(g.bId);
826
+ const build = aRow === bRow ? planUConnector(g.aId, g.bId) : (() => {
827
+ const [up, lo] = aRow < bRow ? [g.aId, g.bId] : [g.bId, g.aId];
828
+ return planConnector(up, lo);
829
+ })();
830
+ planned.push({
831
+ kind: "bond",
832
+ relIds: g.relIds,
833
+ edgeId: Math.max(...g.relIds),
834
+ fromPersonId: g.aId,
835
+ toPersonId: g.bId,
836
+ titles: g.titles,
837
+ lineStyle: g.style,
838
+ build
839
+ });
840
+ }
841
+ const colCenterX = [];
842
+ const colLeftX = [];
843
+ const colRightX = [];
844
+ const rowCenterY = [];
845
+ const rowLabelTopY = [];
846
+ const corridorTopY = [];
847
+ const dims = { width: 0 };
848
+ const gutterLaneCount = /* @__PURE__ */ new Map();
849
+ const corridorLaneCount = Array.from({ length: rowCount }, () => 0);
850
+ const laneCount = (gutter, side) => gutterLaneCount.get(`${gutter}:${side}`) ?? 0;
851
+ const geo = {
852
+ cx: (c) => colCenterX[c],
853
+ cy: (r) => rowCenterY[r],
854
+ glyphRight: (id) => colCenterX[colOrThrow(id)] + NODE_SIZE / 2,
855
+ glyphLeft: (id) => colCenterX[colOrThrow(id)] - NODE_SIZE / 2,
856
+ gutterCenterX: (g) => {
857
+ const left = g < 0 ? PADDING : colRightX[g];
858
+ const right = g >= colCount - 1 ? dims.width - PADDING : colLeftX[g + 1];
859
+ return (left + right) / 2;
860
+ },
861
+ gutterLaneX: (req) => {
862
+ const left = req.gutter < 0 ? PADDING : colRightX[req.gutter];
863
+ const right = req.gutter >= colCount - 1 ? dims.width - PADDING : colLeftX[req.gutter + 1];
864
+ const lCount = laneCount(req.gutter, -1);
865
+ if (req.side === -1) return left + (req.lane + 1) * LANE_H;
866
+ if (req.side === 0) return left + (lCount + req.lane + 1) * LANE_H;
867
+ return right - (req.lane + 1) * LANE_H;
868
+ },
869
+ corridorLaneY: (req) => corridorTopY[req.corridor] + CORRIDOR_BASE / 2 + req.lane * LANE_H,
870
+ /** Y of a dipped serial-union bar (level ≥ 1): below the row's glyph bottom edge. */
871
+ unionDipY: (r, level) => rowCenterY[r] + NODE_SIZE / 2 + level * UNION_STAGGER
872
+ };
873
+ const gutterGroups = /* @__PURE__ */ new Map();
874
+ for (const req of gutterReqs) {
875
+ const k = `${req.gutter}:${req.side}`;
876
+ (gutterGroups.get(k) ?? gutterGroups.set(k, []).get(k)).push(req);
877
+ }
878
+ for (const [k, reqs] of gutterGroups) {
879
+ gutterLaneCount.set(
880
+ k,
881
+ allocateLanes(reqs.map((req) => ({ lo: req.rowLo, hi: req.rowHi, set: (lane) => req.lane = lane })))
882
+ );
883
+ }
884
+ const gutterWidth = (g) => {
885
+ const lanes = laneCount(g, -1) + laneCount(g, 0) + laneCount(g, 1);
886
+ const extra = g === isolatedTailCol - 1 ? ISOLATED_GUTTER_EXTRA : 0;
887
+ if (g === -1 || g === colCount - 1) return lanes > 0 ? COL_PAD + lanes * LANE_H + extra : extra;
888
+ return COL_PAD + lanes * LANE_H + extra;
889
+ };
890
+ let cursorX = PADDING + gutterWidth(-1);
891
+ for (let c = 0; c < colCount; c++) {
892
+ colLeftX[c] = cursorX;
893
+ colCenterX[c] = cursorX + colWidth[c] / 2;
894
+ cursorX += colWidth[c];
895
+ colRightX[c] = cursorX;
896
+ cursorX += gutterWidth(c);
897
+ }
898
+ dims.width = cursorX + PADDING;
899
+ const corridorGroups = /* @__PURE__ */ new Map();
900
+ for (const req of corridorReqs) (corridorGroups.get(req.corridor) ?? corridorGroups.set(req.corridor, []).get(req.corridor)).push(req);
901
+ for (const [k, reqs] of corridorGroups) {
902
+ corridorLaneCount[k] = allocateLanes(
903
+ reqs.map((req) => {
904
+ const [lo, hi] = req.xRange();
905
+ return { lo, hi, set: (lane) => req.lane = lane };
906
+ })
907
+ );
908
+ }
909
+ const corridorHeight = (k) => {
910
+ const lanes = corridorLaneCount[k] ?? 0;
911
+ if (lanes > 0) return CORRIDOR_BASE + lanes * LANE_H;
912
+ return k < rowCount - 1 ? CORRIDOR_BASE : 0;
913
+ };
914
+ const rowDipReserve = (r) => rowDipLevels[r] > 0 ? rowDipLevels[r] * UNION_STAGGER + 6 : 0;
915
+ let cursorY = PADDING;
916
+ for (let r = 0; r < rowCount; r++) {
917
+ rowCenterY[r] = cursorY + NODE_SIZE / 2;
918
+ rowLabelTopY[r] = cursorY + NODE_SIZE + rowDipReserve(r) + LABEL_GAP_TOP;
919
+ cursorY = rowLabelTopY[r] + rowMaxLines[r] * LABEL_LINE_H;
920
+ corridorTopY[r] = cursorY;
921
+ cursorY += corridorHeight(r);
922
+ }
923
+ const width = dims.width;
924
+ const height = cursorY + PADDING;
925
+ const nodes = [...people].sort((a, b) => a.id - b.id).map((p) => {
926
+ const m = measured.get(p.id);
927
+ const r = rowOfPerson.get(p.id);
928
+ const c = colOrThrow(p.id);
929
+ return {
930
+ personId: p.id,
931
+ label: p.label,
932
+ shape: shapeForSex(p.sex),
933
+ deceased: p.deceased,
934
+ cx: colCenterX[c],
935
+ cy: rowCenterY[r],
936
+ size: NODE_SIZE,
937
+ labelLines: m.lines,
938
+ labelTop: rowLabelTopY[r]
939
+ };
940
+ });
941
+ const elements = planned.map((pl) => ({
942
+ kind: pl.kind,
943
+ relIds: pl.relIds,
944
+ edgeId: pl.edgeId,
945
+ fromPersonId: pl.fromPersonId,
946
+ toPersonId: pl.toPersonId,
947
+ points: pl.build(),
948
+ titles: pl.titles,
949
+ lineStyle: pl.lineStyle,
950
+ ...pl.unionStyle !== void 0 ? { unionStyle: pl.unionStyle } : {},
951
+ ...pl.dotted ? { dotted: true } : {}
952
+ }));
953
+ const isolatedPersonIds = people.filter((p) => isIsolated(p.id)).map((p) => p.id).sort((a, b) => a - b);
954
+ return { width, height, nodes, elements, isolatedPersonIds };
955
+ }
956
+ function linearizeBlock(comp, adj) {
957
+ if (comp.length <= 1) return [...comp];
958
+ const inComp = new Set(comp);
959
+ const degree = (id) => adj.get(id).filter((n) => inComp.has(n)).length;
960
+ const endpoints = comp.filter((id) => degree(id) === 1).sort((a, b) => a - b);
961
+ const edgeCount = comp.reduce((s, id) => s + degree(id), 0) / 2;
962
+ const isPath = edgeCount === comp.length - 1 && endpoints.length === 2;
963
+ const order = [];
964
+ const seen = /* @__PURE__ */ new Set();
965
+ if (isPath) {
966
+ let cur = endpoints[0];
967
+ while (cur !== void 0) {
968
+ order.push(cur);
969
+ seen.add(cur);
970
+ cur = adj.get(cur).filter((n) => inComp.has(n)).sort((a, b) => a - b).find((n) => !seen.has(n));
971
+ }
972
+ return order;
973
+ }
974
+ const stack = [Math.min(...comp)];
975
+ while (stack.length > 0) {
976
+ const n = stack.pop();
977
+ if (seen.has(n)) continue;
978
+ seen.add(n);
979
+ order.push(n);
980
+ for (const m of adj.get(n).filter((x) => inComp.has(x)).sort((a, b) => b - a)) {
981
+ if (!seen.has(m)) stack.push(m);
982
+ }
983
+ }
984
+ return order;
985
+ }
986
+
987
+ // src/genogram/svg.ts
988
+ var GLYPH_STROKE = "#52525b";
989
+ var NODE_LABEL_FILL = "#3f3f46";
990
+ var EDGE_INK = "#71717a";
991
+ var STRUCT_WIDTH = 1.5;
992
+ var STRUCT_OPACITY = 0.75;
993
+ var DOTTED_DASH = [2, 3];
994
+ var DOTTED_OPACITY = 0.55;
995
+ var LEGEND_ROW_H = 18;
996
+ var LEGEND_PAD = 16;
997
+ var LEGEND_SWATCH_W = 22;
998
+ var LEGEND_GAP = 14;
999
+ var LEGEND_FONT = 11;
1000
+ function glyphSvg(shape, cx, cy, half) {
1001
+ const stroke = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="2"`;
1002
+ if (shape === "square") {
1003
+ return `<rect x="${cx - half}" y="${cy - half}" width="${half * 2}" height="${half * 2}" rx="4" ${stroke}/>`;
1004
+ }
1005
+ if (shape === "circle") {
1006
+ return `<circle cx="${cx}" cy="${cy}" r="${half}" ${stroke}/>`;
1007
+ }
1008
+ return `<polygon points="${cx},${cy - half} ${cx + half},${cy} ${cx},${cy + half} ${cx - half},${cy}" ${stroke}/>`;
1009
+ }
1010
+ function pathData(points) {
1011
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
1012
+ }
1013
+ function longestHSegment(points) {
1014
+ let best = [points[0], points[points.length - 1]];
1015
+ let bestLen = -1;
1016
+ for (let i = 0; i + 1 < points.length; i++) {
1017
+ const a = points[i];
1018
+ const b = points[i + 1];
1019
+ if (a.y === b.y) {
1020
+ const len = Math.abs(b.x - a.x);
1021
+ if (len > bestLen) {
1022
+ bestLen = len;
1023
+ best = [a, b];
1024
+ }
1025
+ }
1026
+ }
1027
+ return best;
1028
+ }
1029
+ function slashMarks(a, b, count, width) {
1030
+ const mx = (a.x + b.x) / 2;
1031
+ const my = (a.y + b.y) / 2;
1032
+ const dx = b.x - a.x;
1033
+ const dy = b.y - a.y;
1034
+ const len = Math.hypot(dx, dy) || 1;
1035
+ const ux = dx / len;
1036
+ const uy = dy / len;
1037
+ const HALF = 7;
1038
+ const STEP = 4;
1039
+ const offsets = count === 1 ? [0] : [-STEP, STEP];
1040
+ return offsets.map((o) => {
1041
+ const px = mx + ux * o;
1042
+ const py = my + uy * o;
1043
+ return `<line x1="${px - HALF}" y1="${py + HALF}" x2="${px + HALF}" y2="${py - HALF}" stroke="${EDGE_INK}" stroke-width="${width}"/>`;
1044
+ }).join("");
1045
+ }
1046
+ function resolveUnionStyle(el, override) {
1047
+ return override.get(el.edgeId) ?? el.unionStyle ?? {};
1048
+ }
1049
+ function elementSvg(el, override) {
1050
+ const title = `<title>${xmlEscape(el.titles.join("\n"))}</title>`;
1051
+ const pts = el.points;
1052
+ const drawLine = (dash, width, opacity) => {
1053
+ const dashAttr = dash === null ? "" : ` stroke-dasharray="${dash[0]} ${dash[1]}"`;
1054
+ if (pts.length === 2) {
1055
+ return `<line x1="${pts[0].x}" y1="${pts[0].y}" x2="${pts[1].x}" y2="${pts[1].y}" stroke="${EDGE_INK}" stroke-width="${width}" stroke-opacity="${opacity}"${dashAttr}/>`;
1056
+ }
1057
+ return `<path d="${pathData(pts)}" fill="none" stroke="${EDGE_INK}" stroke-width="${width}" stroke-opacity="${opacity}"${dashAttr}/>`;
1058
+ };
1059
+ if (el.kind === "union-bar" || el.kind === "union-elbow") {
1060
+ const style = resolveUnionStyle(el, override);
1061
+ const body = [drawLine(style.dash ?? null, STRUCT_WIDTH, STRUCT_OPACITY)];
1062
+ const slashes = style.slashes ?? 0;
1063
+ if (slashes === 1 || slashes === 2) {
1064
+ const [a, b] = pts.length === 2 ? [pts[0], pts[1]] : longestHSegment(pts);
1065
+ body.push(slashMarks(a, b, slashes, STRUCT_WIDTH));
1066
+ }
1067
+ return `<g data-edge-id="${el.edgeId}">${title}${body.join("")}</g>`;
1068
+ }
1069
+ if (el.kind === "descent" || el.kind === "sibling-bar") {
1070
+ const dash = el.dotted ? DOTTED_DASH : null;
1071
+ const opacity = el.dotted ? DOTTED_OPACITY : STRUCT_OPACITY;
1072
+ return `<g data-edge-id="${el.edgeId}">${title}${drawLine(dash, STRUCT_WIDTH, opacity)}</g>`;
1073
+ }
1074
+ const ink = EDGE_STROKE[el.lineStyle];
1075
+ return `<g data-edge-id="${el.edgeId}">${title}${drawLine(ink.dash, ink.width, ink.opacity)}</g>`;
1076
+ }
1077
+ function genogramLayoutSvg(layout, opts = {}) {
1078
+ const override = opts.unionStyleByRelId ?? /* @__PURE__ */ new Map();
1079
+ const labels = opts.labels ?? GENOGRAM_SVG_LABELS_EN;
1080
+ const parts = [];
1081
+ for (const el of layout.elements) parts.push(elementSvg(el, override));
1082
+ for (const node of layout.nodes) {
1083
+ const half = node.size / 2;
1084
+ const pieces = [
1085
+ `<title>${xmlEscape(node.label)}</title>`,
1086
+ glyphSvg(node.shape, node.cx, node.cy, half)
1087
+ ];
1088
+ if (node.deceased) {
1089
+ pieces.push(
1090
+ `<line x1="${node.cx - half}" y1="${node.cy - half}" x2="${node.cx + half}" y2="${node.cy + half}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`
1091
+ );
1092
+ }
1093
+ const tspans = node.labelLines.map(
1094
+ (line, i) => `<tspan x="${node.cx}" y="${node.labelTop + 10 + i * LABEL_LINE_H}">${xmlEscape(line)}</tspan>`
1095
+ ).join("");
1096
+ pieces.push(
1097
+ `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${LABEL_FONT}" fill="${NODE_LABEL_FILL}">${tspans}</text>`
1098
+ );
1099
+ parts.push(`<g data-individual-id="p${node.personId}">${pieces.join("")}</g>`);
1100
+ }
1101
+ let width = layout.width;
1102
+ let height = layout.height;
1103
+ if (opts.legend !== false && layout.nodes.length > 0) {
1104
+ const entries = [];
1105
+ const shapesUsed = new Set(layout.nodes.map((n) => n.shape));
1106
+ for (const shape of ["square", "circle", "diamond"]) {
1107
+ if (!shapesUsed.has(shape)) continue;
1108
+ entries.push({
1109
+ swatch: (x, y) => glyphSvg(shape, x + LEGEND_SWATCH_W / 2, y, 6),
1110
+ label: labels.shapes[shape]
1111
+ });
1112
+ }
1113
+ if (layout.nodes.some((n) => n.deceased)) {
1114
+ entries.push({
1115
+ swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W / 2, y, 6) + `<line x1="${x + LEGEND_SWATCH_W / 2 - 6}" y1="${y - 6}" x2="${x + LEGEND_SWATCH_W / 2 + 6}" y2="${y + 6}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`,
1116
+ label: labels.deceased
1117
+ });
1118
+ }
1119
+ const stylesUsed = new Set(layout.elements.filter((e) => e.kind === "bond").map((e) => e.lineStyle));
1120
+ for (const style of ["close", "distant", "conflict", "cutoff"]) {
1121
+ if (!stylesUsed.has(style)) continue;
1122
+ const ink = EDGE_STROKE[style];
1123
+ const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
1124
+ entries.push({
1125
+ swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
1126
+ label: labels.bondStyles[style]
1127
+ });
1128
+ }
1129
+ if (layout.isolatedPersonIds.length > 0) {
1130
+ entries.push({
1131
+ swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W / 2, y, 6),
1132
+ label: labels.isolated
1133
+ });
1134
+ }
1135
+ if (entries.length > 0) {
1136
+ const startY = layout.height;
1137
+ const rows = entries.map((entry, i) => {
1138
+ const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
1139
+ const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
1140
+ return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${GLYPH_STROKE}">${xmlEscape(entry.label)}</text>`;
1141
+ });
1142
+ parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
1143
+ const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
1144
+ width = Math.max(width, LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD);
1145
+ height = startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2;
1146
+ }
1147
+ }
1148
+ 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>`;
1149
+ }
1150
+
1151
+ // src/genogram/render.ts
1152
+ var COUPLE_DASH = [6, 4];
1153
+ var UNION_NOTATION = {
1154
+ married: {},
1155
+ // solid bar
1156
+ cohabiting: { dash: COUPLE_DASH },
1157
+ dating: { dash: COUPLE_DASH },
1158
+ unknown: { dash: COUPLE_DASH },
1159
+ separated: { slashes: 1 },
1160
+ // solid + one diagonal slash
1161
+ divorced: { slashes: 2 }
1162
+ // solid + two parallel slashes
1163
+ };
1164
+ function latestUnionPerPair(unions) {
1165
+ const byPair = /* @__PURE__ */ new Map();
1166
+ for (const u of [...unions].sort((a, b) => a.id - b.id)) {
1167
+ const key = `${Math.min(u.personAId, u.personBId)}|${Math.max(u.personAId, u.personBId)}`;
1168
+ byPair.set(key, u);
1169
+ }
1170
+ return [...byPair.values()].sort((a, b) => a.id - b.id);
1171
+ }
1172
+ function genogramSvg(input, opts = {}) {
1173
+ const ids = new Set(input.people.map((p) => p.id));
1174
+ const dedupedUnions = latestUnionPerPair(input.unions).filter(
1175
+ (u) => ids.has(u.personAId) && ids.has(u.personBId)
1176
+ );
1177
+ const layout = computeGenogramLayout(
1178
+ input.people,
1179
+ dedupedUnions,
1180
+ input.parentLinks,
1181
+ input.relationships,
1182
+ opts
1183
+ );
1184
+ const unionStyleByRelId = /* @__PURE__ */ new Map();
1185
+ for (const u of dedupedUnions) {
1186
+ if (u.status === "coparental") continue;
1187
+ unionStyleByRelId.set(UNION_REL_ID_BASE + u.id, UNION_NOTATION[u.status]);
1188
+ }
1189
+ const svg = genogramLayoutSvg(layout, {
1190
+ unionStyleByRelId,
1191
+ ...opts.legend === false ? { legend: false } : {},
1192
+ ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
1193
+ });
1194
+ return { svg, layout };
1195
+ }
1196
+
1197
+ exports.CHAR_W = CHAR_W;
1198
+ exports.EDGE_FONT = EDGE_FONT;
1199
+ exports.EDGE_STROKE = EDGE_STROKE;
1200
+ exports.GENOGRAM_SVG_LABELS_EN = GENOGRAM_SVG_LABELS_EN;
1201
+ exports.GENOGRAM_TITLE_LABELS_EN = GENOGRAM_TITLE_LABELS_EN;
1202
+ exports.KINSHIP_EN = KINSHIP_EN;
1203
+ exports.LABEL_FONT = LABEL_FONT;
1204
+ exports.LABEL_LINE_H = LABEL_LINE_H;
1205
+ exports.NODE_SIZE = NODE_SIZE;
1206
+ exports.PARENT_REL_ID_BASE = PARENT_REL_ID_BASE;
1207
+ exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
1208
+ exports.UNION_NOTATION = UNION_NOTATION;
1209
+ exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
1210
+ exports.UNION_STATUSES = UNION_STATUSES;
1211
+ exports.classifyRelationshipType = classifyRelationshipType;
1212
+ exports.computeGenogramLayout = computeGenogramLayout;
1213
+ exports.estimateTextWidth = estimateTextWidth;
1214
+ exports.genogramLayoutSvg = genogramLayoutSvg;
1215
+ exports.genogramSvg = genogramSvg;
1216
+ exports.latestUnionPerPair = latestUnionPerPair;
1217
+ exports.qualityLineStyle = qualityLineStyle;
1218
+ exports.relationshipTypeTokens = relationshipTypeTokens;
1219
+ exports.wrapLabel = wrapLabel;
1220
+ exports.xmlEscape = xmlEscape;
1221
+ //# sourceMappingURL=index.cjs.map
1222
+ //# sourceMappingURL=index.cjs.map