Files
parenthese/tools/layout-bench/metrics.mjs
T
IliesAllaliandClaude Opus 5 e8e2485dcd Parenthèse
Galaxie familiale : personnes, unions et filiations, photos, vidéos et voix sur chaque personne,
partage par lien avec consultation sans compte et contributions validées par le propriétaire.
Auto-hébergeable avec Docker. Licence PolyForm Noncommercial 1.0.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 15:49:05 +02:00

122 lines
4.8 KiB
JavaScript

// Métriques de qualité d'un layout { children, edges, coupleBarMeta }
// Toutes les positions sont en coordonnées layout (x,y = coin haut-gauche).
import { constants } from './load.mjs'
const { PERSON_W, UNION_W } = constants
function center(n) { return { x: n.x + n.width / 2, y: n.y + n.height / 2 } }
function segIntersect(a1, a2, b1, b2) {
const d = (a2.x - a1.x) * (b2.y - b1.y) - (a2.y - a1.y) * (b2.x - b1.x)
if (Math.abs(d) < 1e-9) return false
const t = ((b1.x - a1.x) * (b2.y - b1.y) - (b1.y - a1.y) * (b2.x - b1.x)) / d
const u = ((b1.x - a1.x) * (a2.y - a1.y) - (b1.y - a1.y) * (a2.x - a1.x)) / d
return t > 1e-6 && t < 1 - 1e-6 && u > 1e-6 && u < 1 - 1e-6
}
export function computeMetrics(result, coupleBarMeta, tMs) {
const nodes = result.children
const byId = new Map(nodes.map(n => [n.id, n]))
const byGen = new Map()
for (const n of nodes) {
if (!byGen.has(n._generation)) byGen.set(n._generation, [])
byGen.get(n._generation).push(n)
}
// Chevauchements (même génération, intervalles X qui se coupent)
let overlaps = 0
for (const list of byGen.values()) {
const sorted = [...list].sort((a, b) => a.x - b.x)
for (let i = 0; i < sorted.length; i++) {
for (let j = i + 1; j < sorted.length; j++) {
if (sorted[j].x >= sorted[i].x + sorted[i].width - 0.5) break
overlaps++
}
}
}
// Arêtes de filiation : segment centre-bas de l'union → centre-haut de l'enfant
const segs = []
let longEdges = 0
let sumDx = 0
for (const e of result.edges) {
const s = byId.get(e.sources[0]); const t = byId.get(e.targets[0])
if (!s || !t) continue
const a = { x: s.x + s.width / 2, y: s.y + s.height }
const b = { x: t.x + t.width / 2, y: t.y }
segs.push({ a, b, id: e.id })
sumDx += Math.abs(b.x - a.x)
if ((t._generation - s._generation) > 1) longEdges++
}
let crossings = 0
for (let i = 0; i < segs.length; i++) {
for (let j = i + 1; j < segs.length; j++) {
// même source = pas un vrai croisement (fratrie)
if (segs[i].a.x === segs[j].a.x && segs[i].a.y === segs[j].a.y) continue
if (segIntersect(segs[i].a, segs[i].b, segs[j].a, segs[j].b)) crossings++
}
}
// Couples : partenaires adjacents ? même Y ?
let couplesSplit = 0, couplesDiffY = 0
const maxAdj = PERSON_W + UNION_W + 2 * 40 + 1 // largeur perso + union + 2 espacements larges
for (const c of coupleBarMeta) {
const p1 = byId.get(c.p1Key); const p2 = byId.get(c.p2Key)
if (!p1 || !p2) continue
if (Math.abs(p1.x - p2.x) > maxAdj) couplesSplit++
if (p1.y !== p2.y) couplesDiffY++
}
// Parents centrés au-dessus des enfants + fratries contiguës
const childrenByUnion = new Map()
for (const e of result.edges) {
if (!childrenByUnion.has(e.sources[0])) childrenByUnion.set(e.sources[0], [])
childrenByUnion.get(e.sources[0]).push(e.targets[0])
}
let parentOffsetSum = 0, parentOffsetN = 0, siblingSplits = 0
for (const [uid, kids] of childrenByUnion) {
const u = byId.get(uid); if (!u) continue
const kn = kids.map(k => byId.get(k)).filter(Boolean)
if (kn.length === 0) continue
const kc = kn.reduce((s, k) => s + center(k).x, 0) / kn.length
parentOffsetSum += Math.abs(center(u).x - kc); parentOffsetN++
if (kn.length >= 2) {
const minX = Math.min(...kn.map(k => k.x)), maxX = Math.max(...kn.map(k => k.x))
const kidSet = new Set(kids)
const gen = kn[0]._generation
const intruders = (byGen.get(gen) || []).filter(n => n._type === 'person' && !kidSet.has(n.id) && n.x > minX && n.x < maxX)
// un conjoint d'un des enfants n'est pas un intrus
const spouses = new Set()
for (const c of coupleBarMeta) {
if (kidSet.has(c.p1Key)) spouses.add(c.p2Key)
if (kidSet.has(c.p2Key)) spouses.add(c.p1Key)
}
if (intruders.some(n => !spouses.has(n.id))) siblingSplits++
}
}
const dxs = segs.map(s => Math.abs(s.b.x - s.a.x)).sort((a, b) => a - b)
const q = (p) => Math.round(dxs.length ? dxs[Math.min(dxs.length - 1, Math.floor(p * dxs.length))] : 0)
const minX = Math.min(...nodes.map(n => n.x)), maxX = Math.max(...nodes.map(n => n.x + n.width))
const minY = Math.min(...nodes.map(n => n.y)), maxY = Math.max(...nodes.map(n => n.y + n.height))
return {
t_ms: Math.round(tMs * 10) / 10,
nodes: nodes.length,
edges: result.edges.length,
overlaps,
crossings,
couples_split: couplesSplit,
couples_diffY: couplesDiffY,
sibling_splits: siblingSplits,
parent_offset_avg: Math.round(parentOffsetN ? parentOffsetSum / parentOffsetN : 0),
edge_dx_avg: Math.round(segs.length ? sumDx / segs.length : 0),
edge_dx_med: q(0.5),
edge_dx_p90: q(0.9),
long_edges: longEdges,
edges_far: dxs.filter(d => d > 800).length,
width: Math.round(maxX - minX),
height: Math.round(maxY - minY),
}
}