Placement : plus jamais d'îlot, belle-famille posée depuis sa place idéale

Retour de Patch sur un arbre de 265 personnes : les familles « posées à
part » (trop loin pour être collées) faisaient une rangée d'îlots reliés
par des pastilles. Plus aucune famille n'est posée à part : une famille
collée garde son fil, aussi long soit-il.

La belle-famille part de sa place idéale (parents juste au-dessus de
l'enfant) et est poussée vers l'extérieur : sur les rangées de l'enfant et
en dessous (sa fratrie et leurs descendants), au-delà de tout ce qui est
déjà posé, jamais sous la ligne d'une fratrie ; sur les rangées au-dessus
(ses ancêtres, de petits couples), au premier trou libre, à côté des autres
ancêtres. Plusieurs belles-familles du même côté s'emboîtent comme des
parenthèses. Une rangée de lignes de fratrie peut prendre cinq niveaux.

Banc : famille réelle de 385, pastilles 2 → 0 ; descendance fictive de
1 454, pastilles 17 → 0 ; les 14 archétypes passent leurs invariants.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Fable 5.1 committed 2026-10-19 15:43:12 +02:00
1 parent 02177d93b3
commit 6950a9c21d
3 files changed
+51 -35

No files matched your search

+49 -33
View File
@@ -32,7 +32,8 @@ import { computeLayout, componentOrderPerGen, chainGap } from './elkLayout'
const NODE_SPACING = 30
const FAMILY_GAP = 60
// Au-delà de cet écart entre la place idéale d'une famille de conjoint et la première place libre, on ne la colle pas
const DETACH_PX = 1440
// Jamais d'îlot (Patch, 19/10) : une famille collée trop loin garde son fil, aussi long soit-il
const DETACH_PX = Infinity
// Le fil d'une filiation union → enfant : une personne d'une autre famille, sur la ligne de l'enfant,
// entre l'aplomb de l'union et l'enfant, est croisée. Frères et sœurs et leurs conjoint(e)s ne comptent pas
@@ -191,17 +192,30 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
return { xs, rows: acc }
}
// Décale un bloc depuis sa position idéale, du côté demandé, jusqu'au-delà de toute l'étendue de `rows`
// sur les rangées partagées : la famille d'un conjoint se pose au bout de la fratrie, jamais dans un trou
function pushOutside(blockRows, ideal, rows, side, gap) {
// Pose une famille de conjoint depuis sa place idéale (parents juste au-dessus de l'enfant), poussée du côté
// demandé : sur les rangées de l'enfant et en dessous (sa fratrie, leurs descendants), au-delà de toute
// l'étendue déjà posée, jamais sous la ligne d'une fratrie ; sur les rangées au-dessus (ses ancêtres, de
// petits couples), au premier trou libre, à côté des autres ancêtres.
function pushHybrid(blockRows, ideal, rows, side, anchorGen, gap) {
let d = ideal
for (const [g, list] of blockRows) {
const occ = rows.get(g)
if (!occ?.length) continue
const l = list[0][0], r = list[list.length - 1][1]
const ol = occ[0][0], or = occ[occ.length - 1][1]
if (side > 0) d = Math.max(d, or + gap - l)
else d = Math.min(d, ol - gap - r)
for (let iter = 0; iter < 500; iter++) {
let push = 0
for (const [g, list] of blockRows) {
const occ = rows.get(g)
if (!occ?.length) continue
if (g >= anchorGen) {
const l = list[0][0] + d, r = list[list.length - 1][1] + d
const ol = occ[0][0], or = occ[occ.length - 1][1]
if (l < or + gap && r > ol - gap) push = Math.max(push, side > 0 ? or + gap - l : r - (ol - gap))
} else {
for (const [l0, r0] of list) for (const [ol, or] of occ) {
const l = l0 + d, r = r0 + d
if (l < or + gap && r > ol - gap) push = Math.max(push, side > 0 ? or + gap - l : r - (ol - gap))
}
}
}
if (push <= 0) break
d += side * push
}
return d
}
@@ -260,36 +274,38 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
const left = b.pending.some(p => p.side < 0), right = b.pending.some(p => p.side > 0)
return left && !right ? -1 : right && !left ? 1 : 0
}
const ordered = unionsOfChain.flatMap(u => {
const kids = unionsOfChain.flatMap(u => {
const group = childBlocks.filter(b => b.unionId === u)
return [...group.filter(b => sideOf(b) < 0), ...group.filter(b => sideOf(b) === 0), ...group.filter(b => sideOf(b) > 0)]
})
// Les belles-familles des enfants entrent dans l'empilement comme des frères de plus, au bout de la
// fratrie du côté de leur enfant : elles ne se posent jamais sous la ligne de cette fratrie, et le
// contour les espace rangée par rangée. Plusieurs du même côté s'emboîtent comme des parenthèses :
// celle dont l'enfant est le plus proche du bout, et le plus bas, se colle la plus près.
const pending = []
kids.forEach((b, i) => { for (const p of (b.pending || [])) pending.push({ ...p, kidIndex: i, anchorGen: byId.get(p.anchorId)._generation }) })
childBlocks.length = 0
childBlocks.push(...ordered)
childBlocks.push(...kids)
const packed = packBlocks(childBlocks, FAMILY_GAP)
const rows = new Map()
childBlocks.forEach((b, i) => { if (!b.ghost) mergeRows(rows, b.rows, packed.xs[i]) })
// Familles des conjoints des enfants (construites par chaque enfant, voir plus bas) : posées au-delà du
// bout de la fratrie entière, du côté du conjoint, la plus proche du couple d'abord. Jamais entre deux frères.
const attachments = []
const pending = []
childBlocks.forEach((b, i) => { for (const p of (b.pending || [])) pending.push({ ...p, anchorX: packed.xs[i] + b.rel(p.anchorId) }) })
for (const side of [1, -1]) {
const list = pending.filter(p => p.side === side).sort((a, b) => side > 0 ? b.anchorX - a.anchorX : a.anchorX - b.anchorX)
for (const p of list) {
const at = p.block.rel(p.anchorId)
const ideal = at == null ? p.anchorX - p.block.width / 2 : p.anchorX - at
const x = pushOutside(p.block.rows, ideal, rows, side, FAMILY_GAP)
if (Math.abs(x - ideal) > DETACH_PX) { detached.push(p.block); continue }
attachedEdges.add(`${parentUnionOf.get(p.anchorId)}>${p.anchorId}`)
attachments.push({ block: p.block, x })
mergeRows(rows, p.block.rows, x)
}
// Du côté droit, la plus près du bout d'abord (enfant le plus à droite, puis le plus bas) ; symétrique à gauche
const rightP = pending.filter(p => p.side > 0).sort((a, b) => b.kidIndex - a.kidIndex || b.anchorGen - a.anchorGen)
const leftP = pending.filter(p => p.side < 0).sort((a, b) => a.kidIndex - b.kidIndex || b.anchorGen - a.anchorGen)
for (const p of [...rightP, ...leftP]) {
const kid = kids[p.kidIndex]
const anchorAbs = packed.xs[p.kidIndex] + kid.rel(p.anchorId)
const at = p.block.rel(p.anchorId)
const ideal = at == null ? anchorAbs : anchorAbs - at
const x = pushHybrid(p.block.rows, ideal, rows, p.side, p.anchorGen, FAMILY_GAP)
attachedEdges.add(`${parentUnionOf.get(p.anchorId)}>${p.anchorId}`)
attachments.push({ block: p.block, x })
mergeRows(rows, p.block.rows, x)
}
// Familles des conjoints de la chaîne pas encore dessinées : construites entières depuis leur ancêtre
// le plus haut, puis remises au bloc du dessus qui les pose au bout de la fratrie, du côté du conjoint.
// le plus haut, puis remises au bloc du dessus qui les empile au bout de la fratrie, du côté du conjoint.
// Le couple se tourne vers cette famille (conjoint du côté où elle se pose). Une famille déjà dessinée
// (reliée deux fois) ne l'est pas une seconde fois : la filiation reste externe, renvoi si son fil croise.
const ownPending = []
@@ -361,7 +377,7 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
const at = p.block.rel(p.anchorId)
const anchorX = rowX + offsets.get(p.anchorId)
const ideal = at == null ? anchorX - p.block.width / 2 : anchorX - at
const x = pushOutside(p.block.rows, ideal, rows, p.side, FAMILY_GAP)
const x = pushHybrid(p.block.rows, ideal, rows, p.side, gen, FAMILY_GAP)
attachedEdges.add(`${parentUnionOf.get(p.anchorId)}>${p.anchorId}`)
attachments.push({ block: p.block, x })
mergeRows(rows, p.block.rows, x)
@@ -393,7 +409,7 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
return {
rows, members, external, gen, rel, pending: ownPending,
chain: [rowX, rowX + rowW],
get width() { return extentOf(rows)[1] },
width: extentOf(rows)[1],
place(x0) {
for (const id of chain) pos.set(id, x0 + rowX + offsets.get(id))
childBlocks.forEach((b, i) => b.place(x0 + kidXs[i]))
@@ -492,7 +508,7 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
lift.set(id, v)
return v
}
for (const r of list) byId.get(r.id)._railLift = Math.min(3, liftOf(r.id))
for (const r of list) byId.get(r.id)._railLift = Math.min(4, liftOf(r.id))
}
return result
+1 -1
View File
@@ -109,7 +109,7 @@ const SIB_STRAIGHT_DX = SIB_R * 2
// Les lignes de fratrie s'emboîtent : une famille collée au conjoint d'un enfant dessine sa ligne sous celle
// de la famille qui l'entoure. Le placement pose le niveau (_railLift, 0 = la plus proche des enfants).
const RAIL_STEP = 8
const RAIL_STEP = 6
function busY(lift, sy) {
return sy + BUS_DY - RAIL_STEP * (lift || 0)
}
+1 -1
View File
@@ -36,7 +36,7 @@ export function computeDefects(result, coupleBarMeta, graph, referenceId = null)
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))
const sx = cx(u), sy = u.y + u.height / 2, by = sy + BUS_DY + (u._railLift != null ? -6 * u._railLift : busOffset(uid))
let l = sx, r = sx
for (const k of ks) {
const ex = cx(k)