Placement : carte de famille et pont, les grosses belles-familles en cluster

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 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Fable 5.1 committed 2026-10-19 16:57:01 +02:00
1 parent 5a069c25c5
commit fb02139929
8 files changed
+156 -100

No files matched your search

+12 -33
View File
@@ -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
+52 -37
View File
@@ -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
@@ -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
+85 -24
View File
@@ -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()
})
}
+2
View File
@@ -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}',
-2
View File
@@ -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()
-2
View File
@@ -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')
+2 -2
View File
@@ -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))