mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-10 15:52:14 +00:00
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:
1 parent
5a069c25c5
commit
fb02139929
8 files changed
+156
-100
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user