Files
parenthese/tools/layout-bench/ascii.mjs
T
IliesAllaliandClaude Fable 5.1 3be95b1a0f Banc du placement v2 : invariants, défauts sur le tracé réel, archétypes en texte, stabilité
Le banc mesure maintenant le code de production (computeBlockLayout), sans
écran, en quatre étages lus dans l'ordre et jamais additionnés :

- invariants qui doivent valoir zéro (chevauchement, couple non adjacent,
  fratrie coupée ou hors de l'ordre choisi, rangée différente de la génération
  de parenté recalculée ici, années de naissance qui redescendent) ;
- défauts comptés sur la géométrie vraiment dessinée (renvois, lignes de
  fratrie au-dessus d'un portrait étranger, croisements, lignes et foyers plus
  larges qu'un écran, proches de la référence visibles dans un écran) ;
- quatorze archétypes (ordre d'une fratrie, issue #67, branche courte, Marc et
  Nadia, petit conjoint, fratrie de douze, remariage, cousins, oncle et nièce,
  parent seul, adoption, deux familles, belle-famille, trois familles
  enchaînées) rendus en texte, témoins dans __snapshots__/archetypes, testés
  par vitest ; deux échecs connus (belle-famille, trois-belles-familles) en
  attendant la règle « le sang de la référence garde son enfant » ;
- stabilité : retirer une feuille ne devrait déplacer que son foyer.

Les modèles ELK écartés en septembre, elk-common et l'ancienne mesure sur
segments droits sont supprimés, avec la dépendance elkjs. Le générateur
d'arbres fictifs (gen-fr.mjs) entre dans le dépôt ; les arbres réels restent
dans fixtures/, ignoré par git.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 14:14:03 +02:00

82 lines
3.9 KiB
JavaScript

// Rendu texte d'un placement : une ligne par rangée, étiquettes centrées sur l'abscisse de chaque nœud,
// fils de fratrie en caractères de boîte entre deux rangées. Lisible dans un terminal, comparable en CI.
//
// Pa ─ • ─ Ma personnes et union (•) d'un couple, sur la même ligne
// │ descente de l'union vers la ligne de fratrie
// ┬────┴────┬ ligne de fratrie, ┴ sous l'union, ┬ au-dessus de chaque enfant
// │ │ descente vers chaque enfant
// A B
// ═ deux lignes de fratrie qui se superposent
// pa (minuscules) petit conjoint (sans parent ni enfant dans l'arbre)
// renvoi : ... filiation dessinée en pastilles, pas en fil
export function renderAscii(result, coupleBarMeta, { px = 20 } = {}) {
const nodes = result.children
if (!nodes.length) return ''
const byId = new Map(nodes.map((n) => [n.id, n]))
const minX = Math.min(...nodes.map((n) => n.x))
const col = (x) => Math.round((x - minX) / px)
const cx = (n) => n.x + n.width / 2
const width = Math.max(...nodes.map((n) => col(n.x + n.width))) + 4
const label = (n) => {
if (n._type === 'union') return n._compact ? '' : '•'
if (n._type === 'unknown') return '?'
const text = String(n._data?.firstName || n.id).slice(0, 6)
return n._compact ? text.toLowerCase() : text
}
const line = () => Array(width).fill(' ')
const put = (arr, c, s) => {
const start = Math.max(0, c - Math.floor(s.length / 2))
for (let i = 0; i < s.length && start + i < arr.length; i++) arr[start + i] = s[i]
}
const gens = [...new Set(nodes.map((n) => n._generation))].sort((a, b) => a - b)
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 out = []
for (const g of gens) {
const row = line()
// traits de couple, puis étiquettes par-dessus
for (const c of coupleBarMeta) {
const p1 = byId.get(c.p1Key), p2 = byId.get(c.p2Key), u = byId.get(c.elkId)
if (!p1 || !p2 || !u || p1._generation !== g) continue
const from = Math.min(col(cx(p1)), col(cx(p2))), to = Math.max(col(cx(p1)), col(cx(p2)))
for (let i = from; i <= to; i++) row[i] = c.virtual ? '┄' : '─'
}
for (const n of nodes) if (n._generation === g) put(row, col(cx(n)), label(n))
out.push(row)
const drop1 = line(), bus = line(), drop2 = line()
const coverage = new Array(width).fill(0)
let any = false
for (const n of nodes) {
if (n._generation !== g || (n._type !== 'union')) continue
const kids = (kidsOf.get(n.id) || []).map((id) => byId.get(id)).filter(Boolean)
if (!kids.length) continue
any = true
const uc = col(cx(n))
const cols = [uc, ...kids.map((k) => col(cx(k)))]
const l = Math.min(...cols), r = Math.max(...cols)
for (let i = l; i <= r; i++) { if (bus[i] === ' ') bus[i] = '─'; coverage[i]++ }
drop1[uc] = '│'
bus[uc] = '┴'
for (const k of kids) { const kc = col(cx(k)); bus[kc] = kc === uc ? '┼' : '┬'; drop2[kc] = '│' }
}
for (let i = 0; i < width; i++) if (coverage[i] > 1 && bus[i] === '─') bus[i] = '═'
if (any) out.push(drop1, bus, drop2)
}
const lines = out.map((l) => l.join('').trimEnd())
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length))
const text = lines.map((l) => l.slice(indent)).join('\n')
const extra = renvois.map((e) => {
const u = byId.get(e.sources[0]), k = byId.get(e.targets[0])
const c = coupleBarMeta.find((m) => m.elkId === u?.id)
const parents = c ? `${label(byId.get(c.p1Key) || {})}+${label(byId.get(c.p2Key) || {})}` : u?.id
return `renvoi : ${parents} → ${label(k || {})}`
})
return [text, ...extra].join('\n')
}