mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-15 08:32:03 +00:00
Avant, la famille d'un conjoint (ses parents, sa fratrie, ses ancêtres) était posée au premier créneau libre, souvent au bout de l'arbre, et la filiation devenait une pastille « Voir ». L'arbre se lisait comme plusieurs arbres reliés par des pastilles. Maintenant chaque famille de conjoint est construite entière depuis son ancêtre le plus haut et collée au conjoint, de son côté, en poussant les voisins pour faire la place. Le conjoint est tourné vers l'extérieur de sa fratrie ; sa propre fratrie est dessinée en miroir s'il faut qu'il finisse au bout, contre le couple ; un enfant déjà dessiné ailleurs garde une place fantôme dans l'ordre de sa fratrie pour le centrage. Les lignées de la personne de référence sont dessinées d'abord (le sang gagne) ; une famille reliée deux fois (cousins, deux grandes lignées) n'est dessinée qu'une fois, l'autre filiation reste une pastille si son fil croiserait une famille ; une famille qu'il faudrait pousser de plus d'un écran est posée à part. L'ordre des lignées ne dépend plus des effectifs (rangée, année, identifiant). Les lignes de fratrie s'emboîtent : la ligne d'une famille qui en entoure une autre passe au-dessus (_railLift posé par le placement, plus de hachage), plus aucune superposition. Mesuré par le banc (tools/layout-bench) : pastilles 4 → 2 sur une famille réelle de 385 personnes, 78 → 12 sur une descendance fictive de 1 454 ; les 14 archétypes passent leurs invariants, Marc et Nadia et l'issue #67 sans aucune pastille ni croisement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
120 lines
5.9 KiB
JavaScript
120 lines
5.9 KiB
JavaScript
// Défauts comptés sur la géométrie vraiment dessinée (traceSibship dans renderers.js) : l'union descend de
|
|
// BUS_DY sous son centre jusqu'à la ligne de fratrie (décalée de 0, 4 ou 8 px selon l'union), file à
|
|
// l'horizontale, puis descend sur chaque enfant. Les renvois (pastilles) ne sont pas dessinés.
|
|
// Tenir en phase avec busY / BUS_STRAIGHT_DX de renderers.js.
|
|
import { kinshipGenerations } from './invariants.mjs'
|
|
|
|
const BUS_DY = 100
|
|
const BUS_STRAIGHT_DX = 30
|
|
const SCREEN = 1440
|
|
const cx = (n) => n.x + n.width / 2
|
|
export function busOffset(id) { let h = 0; for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0; return (Math.abs(h) % 3) * 4 }
|
|
|
|
export function computeDefects(result, coupleBarMeta, graph, referenceId = null) {
|
|
const nodes = result.children
|
|
const byId = new Map(nodes.map((n) => [n.id, n]))
|
|
const kidsOf = new Map()
|
|
const renvois = []
|
|
for (const e of result.edges) {
|
|
if (e._renvoi) { renvois.push(e); continue }
|
|
if (!kidsOf.has(e.sources[0])) kidsOf.set(e.sources[0], [])
|
|
kidsOf.get(e.sources[0]).push(e.targets[0])
|
|
}
|
|
const siblingsOf = new Map()
|
|
for (const [, kids] of kidsOf) for (const k of kids) siblingsOf.set(k, kids)
|
|
const spousesOf = new Map()
|
|
for (const c of coupleBarMeta) {
|
|
if (!spousesOf.has(c.p1Key)) spousesOf.set(c.p1Key, new Set())
|
|
if (!spousesOf.has(c.p2Key)) spousesOf.set(c.p2Key, new Set())
|
|
spousesOf.get(c.p1Key).add(c.p2Key); spousesOf.get(c.p2Key).add(c.p1Key)
|
|
}
|
|
|
|
// Tracé : troncs, lignes de fratrie, descentes
|
|
const H = [], V = []
|
|
const households = []
|
|
for (const [uid, kids] of kidsOf) {
|
|
const u = byId.get(uid)
|
|
const ks = kids.map((k) => byId.get(k)).filter(Boolean)
|
|
if (!u || !ks.length) continue
|
|
const sx = cx(u), sy = u.y + u.height / 2, by = sy + BUS_DY + (u._railLift != null ? -8 * u._railLift : busOffset(uid))
|
|
let l = sx, r = sx
|
|
for (const k of ks) {
|
|
const ex = cx(k)
|
|
if (Math.abs(ex - sx) >= BUS_STRAIGHT_DX) { l = Math.min(l, ex); r = Math.max(r, ex) }
|
|
V.push({ u: uid, x: ex, a: by, b: k.y })
|
|
}
|
|
V.push({ u: uid, x: sx, a: sy, b: by })
|
|
if (r > l) H.push({ u: uid, y: by, a: l, b: r })
|
|
households.push({ union: uid, enfants: ks.length, etendue: Math.round(r - l), gen: u._generation, kids: new Set(kids) })
|
|
}
|
|
let croisements = 0
|
|
const endroits = new Set()
|
|
for (const v of V) for (const h of H) {
|
|
if (h.u === v.u) continue
|
|
if (v.x > h.a + 1 && v.x < h.b - 1 && h.y > v.a + 1 && h.y < v.b - 1) { croisements++; endroits.add(v.u < h.u ? `${v.u}|${h.u}` : `${h.u}|${v.u}`) }
|
|
}
|
|
let superpositions = 0
|
|
for (let i = 0; i < H.length; i++) for (let j = i + 1; j < H.length; j++) {
|
|
const p = H[i], q = H[j]
|
|
if (Math.abs(p.y - q.y) < 6 && Math.min(p.b, q.b) - Math.max(p.a, q.a) > 10) superpositions++
|
|
}
|
|
const cables_ecran = H.filter((h) => h.b - h.a > SCREEN).length
|
|
|
|
// Ligne de fratrie au-dessus d'un portrait étranger (ni enfant, ni conjoint d'un enfant)
|
|
let fils_sur_etranger = 0
|
|
for (const hh of households) {
|
|
const h = H.find((x) => x.u === hh.union)
|
|
if (!h) continue
|
|
// Un seul arbre : la fratrie du conjoint d'un enfant peut se trouver sous la ligne (famille collée), pas un étranger
|
|
const ok = new Set(hh.kids)
|
|
for (const k of hh.kids) for (const s of (spousesOf.get(k) || [])) {
|
|
ok.add(s)
|
|
for (const sib of (siblingsOf.get(s) || [])) { ok.add(sib); for (const ss of (spousesOf.get(sib) || [])) ok.add(ss) }
|
|
}
|
|
if (nodes.some((n) => n._type === 'person' && n._generation === hh.gen + 1 && !ok.has(n.id) && cx(n) > h.a && cx(n) < h.b)) fils_sur_etranger++
|
|
}
|
|
const foyers_larges = households.filter((h) => h.etendue > SCREEN).length
|
|
|
|
// Renvois : combien, et à quelle distance de parenté de la référence le plus proche
|
|
const kin = kinshipGenerations(graph, referenceId)
|
|
const degree = kinshipDegrees(graph, referenceId)
|
|
const renvoi_degre_min = renvois.length ? Math.min(...renvois.map((e) => degree.get(byId.get(e.targets[0])?._data?.id) ?? 99)) : null
|
|
|
|
// Proches à deux degrés de la référence (parents, enfants, conjoint, fratrie, grands-parents, beaux-parents…)
|
|
// visibles dans un écran autour d'elle
|
|
const ref = nodes.find((n) => n._type === 'person' && n._data?.id === referenceId)
|
|
let proches = 0, proches_ecran = 0
|
|
if (ref) {
|
|
for (const n of nodes) {
|
|
if (n._type !== 'person' || n === ref) continue
|
|
const d = degree.get(n._data.id)
|
|
if (d == null || d > 2) continue
|
|
proches++
|
|
if (Math.abs(cx(n) - cx(ref)) <= SCREEN / 2 && Math.abs(n._generation - ref._generation) <= 2) proches_ecran++
|
|
}
|
|
}
|
|
const pires_foyers = households.sort((a, b) => b.etendue - a.etendue).slice(0, 5).map(({ union, enfants, etendue }) => ({ union, enfants, etendue }))
|
|
return {
|
|
renvois: renvois.length, renvoi_degre_min, croisements, endroits_croises: endroits.size, superpositions, fils_sur_etranger, cables_ecran, foyers_larges,
|
|
proches, proches_ecran, conflits: kin.conflicts, pires_foyers,
|
|
}
|
|
}
|
|
|
|
// Degré de parenté (nombre de liens conjoint / parent / enfant) depuis la référence
|
|
export function kinshipDegrees(graph, referenceId) {
|
|
const adj = new Map()
|
|
const add = (a, b) => { if (!adj.has(a)) adj.set(a, []); adj.get(a).push(b) }
|
|
for (const u of graph.unions) if (u.partner1Id && u.partner2Id) { add(u.partner1Id, u.partner2Id); add(u.partner2Id, u.partner1Id) }
|
|
const unionById = new Map(graph.unions.map((u) => [u.id, u]))
|
|
for (const f of graph.filiations) {
|
|
const parents = f.unionId ? [unionById.get(f.unionId)?.partner1Id, unionById.get(f.unionId)?.partner2Id] : [f.parentId]
|
|
for (const p of parents) if (p) { add(p, f.childId); add(f.childId, p) }
|
|
}
|
|
const d = new Map()
|
|
if (referenceId == null) return d
|
|
d.set(referenceId, 0)
|
|
const queue = [referenceId]
|
|
while (queue.length) { const cur = queue.shift(); for (const n of (adj.get(cur) || [])) if (!d.has(n)) { d.set(n, d.get(cur) + 1); queue.push(n) } }
|
|
return d
|
|
}
|