From fb02139929f0c8b0b6f650c0de2cd868154ec9db Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Mon, 19 Oct 2026 16:57:01 +0200 Subject: [PATCH] Placement : carte de famille et pont, les grosses belles-familles en cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patch a écarté le plan arrière (« beaucoup trop d'infos, 150 personnes en plus, illisible ») et retenu la maquette A. Une belle-famille plus large que deux écrans devient un cluster dessiné à part, sur les mêmes rangées, par le même placement. À sa place, au-dessus du conjoint, une carte de famille (nom, quatre portraits, nombre de personnes) reliée à lui par un pont en pointillé. Dans le cluster, le conjoint apparaît dans sa fratrie (écho de la même personne) avec, sous lui, la carte inverse de la lignée qu'il a rejointe. Un clic sur une carte mène de l'autre côté (même mécanique que les pastilles). Le fil des parents vers le conjoint du plan principal est masqué, il va à l'écho. Nouveau type de nœud « family » dans le placement et le rendu, deux constantes de carte, deux textes anglais. Le plan arrière et son calque sont retirés. Co-Authored-By: Claude Fable 5.1 --- frontend/src/components/Galaxy.jsx | 45 ++------ frontend/src/components/galaxy/blockLayout.js | 89 ++++++++------ frontend/src/components/galaxy/constants.js | 3 + frontend/src/components/galaxy/renderers.js | 109 ++++++++++++++---- frontend/src/i18n/en/arbre.js | 2 + tools/layout-bench/defects.mjs | 2 - tools/layout-bench/invariants.mjs | 2 - tools/layout-bench/run.mjs | 4 +- 8 files changed, 156 insertions(+), 100 deletions(-) diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx index 04c6a4d..4d37ed6 100644 --- a/frontend/src/components/Galaxy.jsx +++ b/frontend/src/components/Galaxy.jsx @@ -50,8 +50,6 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig const photoHintRef = useRef(photoHint) useEffect(() => { photoHintRef.current = photoHint }, [photoHint]) const layoutDataRef = useRef(null) - // Calque du plan arrière (grosses belles-familles), composé plus clair et légèrement flou sous le plan avant - const backLayerRef = useRef(null) const [layoutReady, setLayoutReady] = useState(false) const transformRef = useRef({ x: 0, y: 0, scale: 1 }) @@ -813,36 +811,6 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig const cssW = canvas.width / dprRef.current const cssH = canvas.height / dprRef.current const view = { x0: -x / scale, y0: -y / scale, x1: (cssW - x) / scale, y1: (cssH - y) / scale, scale } - const nodeRenderState = { - nodeRandomData: nodeRandomDataRef.current, - imageCache: imageCache.current, - hoverScales: hoverScales.current, - selectedGlowScales: selectedGlowScales.current, - searchGlowScales: searchGlowScales.current, - mediaHoverState: mediaHoverState.current, - entrance, entranceActive, elapsed, - view, - photoHint: photoHintRef.current, - liftScales: liftScalesRef.current, - } - // Plan arrière : les grosses belles-familles, dessinées d'abord sur un calque, plus claires et un peu floues - if (layoutData.children.some((n) => n._plane === 'back')) { - const off = backLayerRef.current || (backLayerRef.current = document.createElement('canvas')) - if (off.width !== canvas.width || off.height !== canvas.height) { off.width = canvas.width; off.height = canvas.height } - const octx = off.getContext('2d') - octx.setTransform(1, 0, 0, 1, 0, 0) - octx.clearRect(0, 0, off.width, off.height) - octx.setTransform(ctx.getTransform()) - drawCoupleLinks(octx, coupleBarMeta, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view, 'back') - drawFiliations(octx, layoutData.edges, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view, 'back') - drawNodes(octx, layoutData, posMap, entrance, entranceActive, elapsed, { ...nodeRenderState, plane: 'back' }) - ctx.save() - ctx.setTransform(1, 0, 0, 1, 0, 0) - ctx.globalAlpha = 0.42 - ctx.filter = 'blur(0.6px)' - ctx.drawImage(off, 0, 0) - ctx.restore() - } drawCoupleLinks(ctx, coupleBarMeta, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view) drawFiliations(ctx, layoutData.edges, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view) drawPathHighlight(ctx, { @@ -857,7 +825,18 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig nodeRandomData: nodeRandomDataRef.current, layoutData, }) - drawNodes(ctx, layoutData, posMap, entrance, entranceActive, elapsed, nodeRenderState) + drawNodes(ctx, layoutData, posMap, entrance, entranceActive, elapsed, { + nodeRandomData: nodeRandomDataRef.current, + imageCache: imageCache.current, + hoverScales: hoverScales.current, + selectedGlowScales: selectedGlowScales.current, + searchGlowScales: searchGlowScales.current, + mediaHoverState: mediaHoverState.current, + entrance, entranceActive, elapsed, + view, + photoHint: photoHintRef.current, + liftScales: liftScalesRef.current, + }) drawRenvoiPills(ctx) // Annotations dessinées APRES les noeuds pour que stickers/texte soient au-dessus // Annotations : ancrer aux personnes proches pour qu'elles suivent le layout diff --git a/frontend/src/components/galaxy/blockLayout.js b/frontend/src/components/galaxy/blockLayout.js index ba0f89f..5f2a280 100644 --- a/frontend/src/components/galaxy/blockLayout.js +++ b/frontend/src/components/galaxy/blockLayout.js @@ -1,4 +1,5 @@ import { computeLayout, componentOrderPerGen, chainGap } from './elkLayout' +import { PERSON_W, PERSON_H, FAMILY_CARD_W, FAMILY_CARD_H } from './constants' // ============================================================ // Layout par blocs familiaux (Reingold-Tilford adapté aux couples) @@ -41,10 +42,9 @@ const RENVOI_FAR_PX = 1440 const PARENT_SHIFT_PX = 400 // Côté d'une belle-famille : le côté extérieur de la branche, sauf si l'autre côté la rapproche d'au moins ça const SIDE_SWITCH_PX = 1440 -// Deux plans (maquette F du 19/10). Une belle-famille plus large que ça ne se colle pas devant : elle passe sur -// le plan arrière, dessinée par le même placement, plus petite, ancrée à son conjoint -const BACK_PX = 2880 -const BACK_SCALE = 0.55 +// Carte de famille et pont (maquette A, retenue le 19/10). Une belle-famille plus large que ça ne se colle pas à +// son enfant : elle devient un cluster dessiné à part, et une carte prend sa place au-dessus du conjoint +const CLUSTER_PX = 2880 // 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 @@ -187,7 +187,8 @@ export function computeBlockLayout(graphData = null, referenceId = null) { const placed = new Set() const externalEdges = new Set() // `${union}>${enfant}` : enfant dessiné sous une autre lignée const detached = [] // familles qu'on n'a pas pu coller à leur enfant (trop loin) : posées à part, renvoi - const backFamilies = [] // familles de conjoints du plan arrière : { block, anchorId } + const clusters = [] // grosses belles-familles dessinées à part : { block, anchorId, cardId, echoId, mainCardId } + const rowStep = (() => { const ys = [...new Set(nodes.map(n => n.y))].sort((a, b) => a - b); return ys.length > 1 ? ys[1] - ys[0] : PERSON_H + 60 })() const attachedEdges = new Set() // `${union}>${enfant}` : fil d'une famille collée à son enfant, toujours dessiné // Imbrique des blocs de gauche à droite, rangée par rangée @@ -312,7 +313,22 @@ export function computeBlockLayout(graphData = null, referenceId = null) { anchor = { kid: id, end: side < 0 ? 'right' : 'left' } const block = buildBlock(top, null, side) anchor = saved - if (block.width > BACK_PX) { backFamilies.push({ block, anchorId: id }); continue } + if (block.width > CLUSTER_PX) { + // La famille devient un cluster ; ici, une carte de famille tient la place de ses parents + const A = byId.get(id) + const cardId = `fam-${id}`, echoId = `echo-${id}`, mainCardId = `fam-main-${id}` + const partners = [byId.get(pu)?._data?.p1Key, byId.get(pu)?._data?.p2Key].map(k => byId.get(k)?._data).filter(Boolean) + const label = partners.find(d => d.lastName)?.lastName || A._data?.birthName || A._data?.lastName || '' + const people = [...block.members].map(m => byId.get(m)).filter(n => n?._type === 'person') + const portraits = [...people.filter(n => n._data?.photo), ...people.filter(n => !n._data?.photo)].slice(0, 4).map(n => n._data) + nodes.push({ id: cardId, _type: 'family', width: FAMILY_CARD_W, height: FAMILY_CARD_H, _generation: gen - 1, y: A.y - rowStep + (PERSON_H - FAMILY_CARD_H) / 2, _data: { label, count: people.length, portraits, targetId: echoId, stem: 'down', stemTo: id } }) + byId.set(cardId, nodes[nodes.length - 1]) + const card = { rows: new Map([[gen - 1, [[0, FAMILY_CARD_W]]]]), members: new Set([cardId]), external: [], gen: gen - 1, rel: (x) => (x === cardId ? 0 : null), chain: [0, FAMILY_CARD_W], width: FAMILY_CARD_W, place(x0) { pos.set(cardId, x0) } } + clusters.push({ block, anchorId: id, pu, cardId, echoId, mainCardId }) + built.push({ block: card, anchorId: id, side }) + if (chain.length === 3) orient(chain, chain[0] === id ? chain[2] : chain[0], side) + continue + } built.push({ block, anchorId: id, side }) if (chain.length === 3) orient(chain, chain[0] === id ? chain[2] : chain[0], side) } @@ -504,43 +520,42 @@ export function computeBlockLayout(graphData = null, referenceId = null) { for (const r of roots) if (!placed.has(r)) free.push(buildBlock(r)) for (const c of chains) if (!placed.has(c)) free.push(buildBlock(c)) free.push(...detached) + // Clusters : chaque grosse belle-famille à part, sur les mêmes rangées. Le conjoint y apparaît dans sa fratrie + // (écho de la même personne) avec, sous lui, la carte de la lignée qu'il a rejointe. Les deux cartes se répondent. + const clusterStart = free.length + free.push(...clusters.map(c => c.block)) const forest = packBlocks(free, FAMILY_GAP * 2) free.forEach((b, i) => b.place(forest.xs[i])) + const referenceNode = result.referenceId != null ? byId.get(`p-${result.referenceId}`) : null + const inCluster = new Set(clusters.flatMap(c => [...c.block.members])) + const mainPeople = nodes.filter(n => n._type === 'person' && !inCluster.has(n.id) && !n._echo) + clusters.forEach((c, i) => { + const A = byId.get(c.anchorId) + const ghostX = c.block.rel(c.anchorId) + const x0 = forest.xs[clusterStart + i] + if (ghostX == null) return + pos.set(c.echoId, x0 + ghostX) + nodes.push({ id: c.echoId, _type: 'person', _data: A._data, width: PERSON_W, height: PERSON_H, _generation: A._generation, y: A.y, _echo: true, _echoOf: c.anchorId }) + byId.set(c.echoId, nodes[nodes.length - 1]) + const mainChain = chainOf.get(c.anchorId) || [] + const around = [...mainChain.filter(id => byId.get(id)._type === 'person' && id !== c.anchorId), ...mainChain.flatMap(id => childrenOfUnion.get(id) || [])].map(id => byId.get(id)?._data).filter(Boolean) + const mainLabel = referenceNode?._data?.lastName || around[0]?.lastName || '' + nodes.push({ id: c.mainCardId, _type: 'family', width: FAMILY_CARD_W, height: FAMILY_CARD_H, _generation: A._generation + 1, y: A.y + rowStep + (PERSON_H - FAMILY_CARD_H) / 2, _data: { label: mainLabel, count: mainPeople.length, portraits: around.slice(0, 4), targetId: c.anchorId, stem: 'up', stemTo: c.echoId } }) + byId.set(c.mainCardId, nodes[nodes.length - 1]) + pos.set(c.mainCardId, pos.get(c.echoId) + (PERSON_W - FAMILY_CARD_W) / 2) + // Le fil des parents va à l'écho dans le cluster ; vers le conjoint du plan principal, c'est la carte qui parle + for (const e of [...result.edges]) { + if (e.targets[0] !== c.anchorId || !c.block.members.has(e.sources[0])) continue + e._hidden = true + result.edges.push({ ...e, id: `${e.id}#echo`, targets: [c.echoId], _hidden: false }) + } + }) // Réinjecter le X, normalisé à 0 ; Y inchangé (rangées) let minX = Infinity for (const n of nodes) if (pos.has(n.id)) minX = Math.min(minX, pos.get(n.id)) for (const n of nodes) if (pos.has(n.id)) n.x = pos.get(n.id) - minX - // Plan arrière : chaque grosse belle-famille est posée à l'échelle BACK_SCALE autour de son conjoint pivot, - // la place fantôme du conjoint dans sa fratrie d'origine venant coïncider avec lui. Ses parents sont donc - // juste au-dessus de lui, en retrait, et toute sa famille se déploie derrière le plan avant. - for (const { block, anchorId } of backFamilies) { - const A = byId.get(anchorId) - if (!A || A.x == null) continue - block.place(0) - const ghostX = block.rel(anchorId) - if (ghostX == null) continue - const acx = A.x + A.width / 2, acy = A.y + A.height / 2 - const gcx = ghostX + A.width / 2, gcy = acy - for (const m of block.members) { - const n = byId.get(m) - if (!n || !pos.has(m)) continue - const lcx = pos.get(m) + n.width / 2, lcy = n.y + n.height / 2 - const cx = acx + (lcx - gcx) * BACK_SCALE, cy = acy + (lcy - gcy) * BACK_SCALE - n.x = cx - n.width / 2 - n.y = cy - n.height / 2 - n._plane = 'back' - n._scale = BACK_SCALE - } - } - const backIds = new Set(nodes.filter(n => n._plane === 'back').map(n => n.id)) - for (const e of result.edges) { - const s = backIds.has(e.sources[0]), k = backIds.has(e.targets[0]) - if (s && k) e._back = true - else if (s && !k) e._bridge = true // des parents du plan arrière à leur enfant du plan avant - } - // Arête vers un enfant dessiné sous l'autre lignée : renvoi (voir drawFiliations) seulement si le fil // croise une autre famille. Plusieurs renvois sous la même union : pastilles empilées (_renvoiIndex) // Le fil descend de l'union puis file sur la ligne de la fratrie jusqu'à l'enfant (traceSibship) : @@ -559,7 +574,7 @@ export function computeBlockLayout(graphData = null, referenceId = null) { const renvoisByUnion = new Map() for (const e of result.edges) { const u = e.sources[0], c = e.targets[0] - if (e._back || e._bridge) continue + if (e._hidden) continue if (!externalEdges.has(`${u}>${c}`) || attachedEdges.has(`${u}>${c}`)) continue // Famille reliée deux fois à l'arbre (dessinée sous l'autre lien) : pastille si le fil croiserait une // famille, ou s'il devrait traverser plus d'un écran @@ -576,7 +591,7 @@ export function computeBlockLayout(graphData = null, referenceId = null) { // _railLift plus grand = plus haut ; 0 = la ligne la plus proche des enfants. const rails = new Map() for (const e of result.edges) { - if (e._renvoi || e._back || e._bridge) continue + if (e._renvoi || e._hidden) continue const u = byId.get(e.sources[0]), k = byId.get(e.targets[0]) if (!u || !k) continue const ux = u.x + u.width / 2 diff --git a/frontend/src/components/galaxy/constants.js b/frontend/src/components/galaxy/constants.js index 5a90fdc..13e9773 100644 --- a/frontend/src/components/galaxy/constants.js +++ b/frontend/src/components/galaxy/constants.js @@ -16,6 +16,9 @@ export const COMPACT_SCALE = 0.6 export const COMPACT_R = PERSON_R * COMPACT_SCALE export const COMPACT_W = 40 export const COMPACT_JOIN_GAP = -18 +// Carte de famille : tient la place d'un couple au-dessus d'un conjoint dont la famille est dessinée à part +export const FAMILY_CARD_W = 200 +export const FAMILY_CARD_H = 76 export const MAX_ORBIT_MEDIAS = 3 export const ORBIT_RADIUS = 55 export const ORBIT_MEDIA_SIZE = 22 diff --git a/frontend/src/components/galaxy/renderers.js b/frontend/src/components/galaxy/renderers.js index f45e1ad..505ead6 100644 --- a/frontend/src/components/galaxy/renderers.js +++ b/frontend/src/components/galaxy/renderers.js @@ -24,13 +24,10 @@ const NICKNAME_MAX_W = 150 // ============================================================ // Rayon d'un nœud : inconnu, conjoint isolé (petit) ou personne export function nodeRadius(node) { - const scale = node?._scale || 1 - if (node?._type === 'unknown') return UNKNOWN_R * scale - if (node?._compact) return COMPACT_R * scale - return PERSON_R * scale + if (node?._type === 'unknown') return UNKNOWN_R + if (node?._compact) return COMPACT_R + return PERSON_R } -// Plan d'un nœud : 'front' (la lignée) ou 'back' (une grosse belle-famille, en retrait) -export const planeOf = (node) => node?._plane || 'front' function rectInView(view, minX, minY, maxX, maxY) { if (!view) return true @@ -113,17 +110,17 @@ 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 = 6 -function busY(lift, sy, scale = 1) { - return sy + (BUS_DY - RAIL_STEP * (lift || 0)) * scale +function busY(lift, sy) { + return sy + BUS_DY - RAIL_STEP * (lift || 0) } // Une union, toute sa fratrie : un seul tracé. Tronc depuis l'union jusqu'à la barre, coins arrondis // de même rayon à chaque jonction, puis descente vers chaque enfant. Chaque enfant a sa propre trajectoire // (sur la même barre), donc le surlignage du chemin suit exactement la même géométrie. -export function traceSibship(ctx, sourceId, sourcePos, items, lift = 0, scale = 1) { +export function traceSibship(ctx, sourceId, sourcePos, items, lift = 0) { const sx = sourcePos.cx, sy = sourcePos.cy - const by = busY(lift, sy, scale) - const rr = Math.min(SIB_R * scale, by - sy - 2) + const by = busY(lift, sy) + const rr = Math.min(SIB_R, by - sy - 2) ctx.beginPath() for (const { cx: ex, cy: ey } of items) { const dx = ex - sx @@ -150,9 +147,8 @@ export function traceSibship(ctx, sourceId, sourcePos, items, lift = 0, scale = // ============================================================ // Courbes de couple (Bézier) // ============================================================ -export function drawCoupleLinks(ctx, coupleBarMeta, posMap, nodeMap, nodeRandomData, entrance, entranceActive, elapsed, view = null, plane = 'front') { +export function drawCoupleLinks(ctx, coupleBarMeta, posMap, nodeMap, nodeRandomData, entrance, entranceActive, elapsed, view = null) { coupleBarMeta.forEach(({ elkId, p1Key, p2Key, virtual }) => { - if (planeOf(nodeMap.get(p1Key)) !== plane) return const p1Pos = posMap.get(p1Key) const p2Pos = posMap.get(p2Key) const unionPos = posMap.get(elkId) @@ -254,6 +250,74 @@ function drawRenvoi(ctx, edge, sourcePos, targetPos, targetNode, childR, view) { ) } +// ============================================================ +// Carte de famille (maquette A, 19/10) : une belle-famille dessinée à part est représentée, à la place de +// ses parents au-dessus du conjoint, par une carte (nom, portraits, nombre) reliée au conjoint par un +// pont. Dans le cluster, la carte inverse se trouve sous l'écho du conjoint. Un clic mène de l'autre côté. +// ============================================================ +const CARD_FONT = '500 13px "Newsreader", Georgia, serif' +const CARD_SMALL = '400 10px "DM Sans", system-ui, sans-serif' +function drawFamilyCard(ctx, node, pos, posMap, progress, view) { + const { cx, cy } = pos + const w = node.width, h = node.height + const scale = view?.scale ?? 1 + const alpha = Math.min(1, progress * 1.5) + // Pont vers la personne qu'elle remplace ou qu'elle prolonge + const to = posMap.get(node._data.stemTo) + if (to) { + ctx.save() + ctx.globalAlpha = alpha + ctx.strokeStyle = FIL.color + ctx.lineWidth = FIL.width + ctx.setLineDash([3, 4]) + ctx.lineCap = 'round' + ctx.beginPath() + if (node._data.stem === 'down') { ctx.moveTo(cx, cy + h / 2); ctx.lineTo(to.cx, to.cy - PERSON_R - 2) } + else { ctx.moveTo(cx, cy - h / 2); ctx.lineTo(to.cx, to.cy + PERSON_R + 2) } + ctx.stroke() + ctx.restore() + } + ctx.save() + ctx.globalAlpha = alpha + ctx.fillStyle = '#FCF3EF' + ctx.strokeStyle = '#EDB6A4' + ctx.lineWidth = 0.9 + ctx.beginPath() + ctx.roundRect(cx - w / 2, cy - h / 2, w, h, 14) + ctx.fill() + ctx.stroke() + // Quatre petits portraits, en mosaïque + const ps = node._data.portraits || [] + ps.forEach((p, i) => { + const px = cx - w / 2 + 16 + (i % 2) * 24, py = cy - 22 + Math.floor(i / 2) * 24 + ctx.fillStyle = '#F2EFEA' + ctx.strokeStyle = '#E3DED6' + ctx.lineWidth = 0.7 + ctx.beginPath() + ctx.roundRect(px, py, 20, 20, 3) + ctx.fill() + ctx.stroke() + ctx.fillStyle = COLORS.textLight + ctx.font = '400 8px "Newsreader", Georgia, serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(`${(p.firstName || '')[0] || ''}${(p.lastName || '')[0] || ''}`, px + 10, py + 10.5) + }) + if (scale > LOD_LABEL_FADE_OUT) { + ctx.fillStyle = COLORS.text + ctx.font = CARD_FONT + ctx.textAlign = 'left' + ctx.textBaseline = 'middle' + ctx.fillText(node._data.label ? t('Famille {name}', { name: node._data.label }) : t('Famille'), cx - w / 2 + 72, cy - 8) + ctx.fillStyle = '#93402A' + ctx.font = CARD_SMALL + ctx.fillText(t('{n} personnes', { n: node._data.count }), cx - w / 2 + 72, cy + 10) + } + ctx.restore() + const target = posMap.get(node._data.targetId) + if (target && alpha > 0.5) renvoiHitAreas.push({ x0: cx - w / 2, y0: cy - h / 2, x1: cx + w / 2, y1: cy + h / 2, targetX: target.cx, targetY: target.cy }) +} + // Après drawNodes : les pastilles passent au-dessus des portraits et des souvenirs export function drawRenvoiPills(ctx) { renvoiPills.forEach(p => drawRenvoiPill(ctx, p)) @@ -263,15 +327,16 @@ export function drawRenvoiPills(ctx) { // ============================================================ // Filiations (Bézier union → enfant) // ============================================================ -export function drawFiliations(ctx, edges, posMap, nodeMap, nodeRandomData, entrance, entranceActive, elapsed, view = null, plane = 'front') { - if (plane === 'front') { renvoiHitAreas.length = 0; renvoiPills.length = 0 } +export function drawFiliations(ctx, edges, posMap, nodeMap, nodeRandomData, entrance, entranceActive, elapsed, view = null) { + renvoiHitAreas.length = 0 + renvoiPills.length = 0 if (!edges) return const sibships = new Map() edges.forEach(edge => { + if (edge._hidden) return const sourceId = edge.sources[0] const targetId = edge.targets[0] - if (planeOf(nodeMap.get(sourceId)) !== plane) return const sourcePos = posMap.get(sourceId) const targetPos = posMap.get(targetId) if (!sourcePos || !targetPos) return @@ -316,7 +381,7 @@ export function drawFiliations(ctx, edges, posMap, nodeMap, nodeRandomData, entr ctx.lineCap = 'round' const p = progress / items.length ctx.globalAlpha = Math.min(1, p * 1.5) * p - traceSibship(ctx, sourceId, sourcePos, items, nodeMap.get(sourceId)?._railLift, nodeMap.get(sourceId)?._scale || 1) + traceSibship(ctx, sourceId, sourcePos, items, nodeMap.get(sourceId)?._railLift) ctx.globalAlpha = 1 ctx.setLineDash([]) } @@ -1039,17 +1104,12 @@ function drawImageCover(ctx, img, x, y, w, h, crop = null) { // ============================================================ export function drawNodes(ctx, layoutData, posMap, entrance, entranceActive, elapsed, renderState) { const view = renderState.view || null - const plane = renderState.plane || 'front' - if (plane === 'front') photoPlaceholderHitAreas.length = 0 + photoPlaceholderHitAreas.length = 0 layoutData.children.forEach(node => { - if (planeOf(node) !== plane) return const pos = posMap.get(node.id) if (!pos) return const { cx, cy } = pos if (!pointInView(view, cx, cy)) return - // Plan arrière : le nœud est dessiné à son échelle, autour de son centre - const scale = node._scale || 1 - if (scale !== 1) { ctx.save(); ctx.translate(cx, cy); ctx.scale(scale, scale); ctx.translate(-cx, -cy) } let nodeProgress = 1 if (entranceActive) { @@ -1062,6 +1122,8 @@ export function drawNodes(ctx, layoutData, posMap, entrance, entranceActive, ela if (node._type === 'person') { drawPersonNode(ctx, node, pos, nodeProgress, renderState) + } else if (node._type === 'family') { + drawFamilyCard(ctx, node, pos, posMap, nodeProgress, view) } else if (node._type === 'union') { ctx.globalAlpha = nodeProgress ctx.fillStyle = FIL.dot @@ -1098,7 +1160,6 @@ export function drawNodes(ctx, layoutData, posMap, entrance, entranceActive, ela ctx.fillText(t('Inconnu'), 0, UNKNOWN_R + 4) ctx.restore() } - if (scale !== 1) ctx.restore() }) } diff --git a/frontend/src/i18n/en/arbre.js b/frontend/src/i18n/en/arbre.js index 4920545..27f43a8 100644 --- a/frontend/src/i18n/en/arbre.js +++ b/frontend/src/i18n/en/arbre.js @@ -1,5 +1,7 @@ // Traductions anglaises, clé = texte français exact passé à t(). export default { + 'Famille {name}': 'The {name} family', + 'Famille': 'Family', // Arbre sur canvas (components/galaxy/renderers.js) 'Voir les parents': 'See parents', 'Voir {firstName}': 'See {firstName}', diff --git a/tools/layout-bench/defects.mjs b/tools/layout-bench/defects.mjs index 20233c8..a34e327 100644 --- a/tools/layout-bench/defects.mjs +++ b/tools/layout-bench/defects.mjs @@ -11,8 +11,6 @@ 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) { - // Le plan arrière (grosses belles-familles, maquette F) se mesure à part : ici, le plan avant seulement - result = { ...result, children: result.children.filter((n) => n._plane !== 'back'), edges: result.edges.filter((e) => !e._back && !e._bridge) } const nodes = result.children const byId = new Map(nodes.map((n) => [n.id, n])) const kidsOf = new Map() diff --git a/tools/layout-bench/invariants.mjs b/tools/layout-bench/invariants.mjs index df78f5f..38de306 100644 --- a/tools/layout-bench/invariants.mjs +++ b/tools/layout-bench/invariants.mjs @@ -60,8 +60,6 @@ export function bloodRelatives(graph, referenceId) { const birthYear = (p) => { const raw = p?.birthYear ?? p?.birthDate; const y = raw != null ? parseInt(String(raw).slice(0, 4), 10) : NaN; return Number.isFinite(y) ? y : null } export function checkInvariants(result, coupleBarMeta, graph, referenceId = null) { - // Le plan arrière (grosses belles-familles, maquette F) se mesure à part : ici, le plan avant seulement - result = { ...result, children: result.children.filter((n) => n._plane !== 'back'), edges: result.edges.filter((e) => !e._back && !e._bridge) } const nodes = result.children const byId = new Map(nodes.map((n) => [n.id, n])) const persons = nodes.filter((n) => n._type === 'person') diff --git a/tools/layout-bench/run.mjs b/tools/layout-bench/run.mjs index 31459f9..8e58480 100644 --- a/tools/layout-bench/run.mjs +++ b/tools/layout-bench/run.mjs @@ -37,7 +37,7 @@ for (const name of names) { const nodes = result.children const largeur = Math.round(Math.max(...nodes.map((n) => n.x + n.width)) - Math.min(...nodes.map((n) => n.x))) const { pires_foyers, ...defauts } = def - const row = { arbre: name, personnes: nodes.filter((n) => n._type === 'person').length, arriere: nodes.filter((n) => n._type === 'person' && n._plane === 'back').length, ms, ...inv, ...defauts, ...stab, largeur } + const row = { arbre: name, personnes: nodes.filter((n) => n._type === 'person').length, ms, ...inv, ...defauts, ...stab, largeur } row.invariants = HARD.every((k) => inv[k] === 0) ? 'ok' : 'FAUX' rows.push({ ...row, pires_foyers }) if (tree.archetype) { @@ -48,7 +48,7 @@ for (const name of names) { renderSvg(result, coupleBarMeta, `${OUT}${name}.svg`, name) } } -const shown = ['arbre', 'personnes', 'arriere', 'invariants', 'rangees_fausses', 'conflits', 'fratries_coupees', 'renvois', 'renvois_sang', 'fils_sur_etranger', 'croisements', 'superpositions', 'cables_ecran', 'foyers_larges', 'proches_ecran', 'proches', 'max_bouges', 'max_deplacement_px', 'largeur', 'ms'] +const shown = ['arbre', 'personnes', 'invariants', 'rangees_fausses', 'conflits', 'fratries_coupees', 'renvois', 'renvois_sang', 'fils_sur_etranger', 'croisements', 'superpositions', 'cables_ecran', 'foyers_larges', 'proches_ecran', 'proches', 'max_bouges', 'max_deplacement_px', 'largeur', 'ms'] console.table(rows.map((r) => Object.fromEntries(shown.filter((k) => k in r).map((k) => [k, r[k]])))) for (const r of rows) if (r.pires_foyers?.length && r.pires_foyers[0].etendue > 1440) console.log(`${r.arbre} : foyers les plus étendus`, r.pires_foyers.map((h) => `${h.enfants} enfants sur ${h.etendue} px`).join(' · ')) writeFileSync(`${OUT}rapport-${tag}.json`, JSON.stringify(rows, null, 2))