diff --git a/frontend/src/components/galaxy/blockLayout.js b/frontend/src/components/galaxy/blockLayout.js index 93b4133..c20275b 100644 --- a/frontend/src/components/galaxy/blockLayout.js +++ b/frontend/src/components/galaxy/blockLayout.js @@ -1,5 +1,4 @@ import { computeLayout, componentOrderPerGen, chainGap } from './elkLayout' -import { RENVOI_MIN_DX } from './constants' // ============================================================ // Layout par blocs familiaux (Reingold-Tilford adapté aux couples) @@ -27,6 +26,18 @@ import { RENVOI_MIN_DX } from './constants' const NODE_SPACING = 30 const FAMILY_GAP = 60 +// 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 conjoint(e)s de l'enfant ne comptent pas. +export function filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, u, c) { + const byIdLocal = new Map(nodes.map(n => [n.id, n])) + const kid = byIdLocal.get(c) + const cx = (id) => byIdLocal.get(id).x + byIdLocal.get(id).width / 2 + const excluded = new Set([c, ...(childrenOfUnion.get(u) || []), ...(spouseKeysOf.get(c) || [])]) + const lo = Math.min(cx(u), cx(c)), hi = Math.max(cx(u), cx(c)) + return nodes.some(n => n._type === 'person' && n._generation === kid._generation && !excluded.has(n.id) + && n.x + n.width > lo && n.x < hi) +} + export function computeBlockLayout(graphData = null) { const result = computeLayout(graphData) const nodes = result.children @@ -209,13 +220,25 @@ export function computeBlockLayout(graphData = null) { 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 - // Arête longue vers un enfant placé sous l'autre lignée : dessinée en renvoi (voir drawFiliations) - // Plusieurs renvois sous la même union : pastilles empilées (_renvoiIndex) - const centerX = (id) => byId.get(id).x + byId.get(id).width / 2 + // Arête vers un enfant placé 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 (traceBus) : + // il ne traverse une autre famille que si une personne de cette famille se trouve sur cette ligne, + // entre l'aplomb de l'union et l'enfant. Frères et sœurs et conjoint(e)s de l'enfant ne comptent pas. + const spouseKeysOf = new Map() + for (const n of nodes) { + if (n._type !== 'union' || !n._data?.p1Key || !n._data?.p2Key) continue + const { p1Key, p2Key } = n._data + if (!spouseKeysOf.has(p1Key)) spouseKeysOf.set(p1Key, new Set()) + if (!spouseKeysOf.has(p2Key)) spouseKeysOf.set(p2Key, new Set()) + spouseKeysOf.get(p1Key).add(p2Key) + spouseKeysOf.get(p2Key).add(p1Key) + } + const crossesOtherFamily = (u, c) => filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, u, c) const renvoisByUnion = new Map() for (const e of result.edges) { const u = e.sources[0], c = e.targets[0] - if (!externalEdges.has(`${u}>${c}`) || Math.abs(centerX(u) - centerX(c)) <= RENVOI_MIN_DX) continue + if (!externalEdges.has(`${u}>${c}`) || !crossesOtherFamily(u, c)) continue e._renvoi = true e._renvoiIndex = renvoisByUnion.get(u) || 0 renvoisByUnion.set(u, e._renvoiIndex + 1) diff --git a/frontend/src/components/galaxy/blockLayout.test.js b/frontend/src/components/galaxy/blockLayout.test.js index 5120a6a..80e1148 100644 --- a/frontend/src/components/galaxy/blockLayout.test.js +++ b/frontend/src/components/galaxy/blockLayout.test.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { computeBlockLayout } from './blockLayout' +import { computeBlockLayout, filCroiseAutreFamille } from './blockLayout' import { COMPACT_W, PERSON_W } from './constants' const person = (id) => ({ id, firstName: id, lastName: '' }) @@ -60,3 +60,50 @@ describe('computeBlockLayout, ordre de la fratrie', () => { expect(order(graphWith([1, 1, 1]))).toEqual(['a', 'b', 'c']) }) }) + +describe('computeBlockLayout, renvoi seulement si le fil croise une autre famille', () => { + // Structure type : l'enfant (P) a sa fratrie à côté, son conjoint (Z) est dans la lignée de l'autre parent + // Jacqueline + Roger ont 6 enfants, dont Marie, en couple avec Philippe ; Marcel + Yvette ont 5 enfants, dont Philippe + const graph = { + persons: ['jac', 'rog', 'marie', 'philippe', 'anais', 'greg', 'marcel', 'yvette', 'severine', 'isabelle', 'laurent', 'stephane', 'k1', 'k2', 'k3', 'k4', 'k5'].map(person), + unions: [union('uJR', 'jac', 'rog'), union('uMP', 'marie', 'philippe'), union('uMY', 'marcel', 'yvette')], + filiations: [ + ...['k1', 'k2', 'k3', 'k4', 'k5'].map((k, i) => fil(`f${k}`, k, 'uJR', i + 1)), fil('fmarie', 'marie', 'uJR', 6), + fil('fa', 'anais', 'uMP'), fil('fg', 'greg', 'uMP'), + fil('fs', 'severine', 'uMY', 1), fil('fi', 'isabelle', 'uMY', 2), fil('fl', 'laurent', 'uMY', 3), + fil('fst', 'stephane', 'uMY', 4), fil('fp', 'philippe', 'uMY', 5), + ], + } + + it('garde le fil vers Philippe quand la fratrie de son autre famille est à côté de lui', () => { + const { edges } = computeBlockLayout(graph) + expect(edges.find(e => e.sources[0] === 'u-uMY' && e.targets[0] === 'p-philippe')._renvoi).toBeUndefined() + }) +}) + +describe('filCroiseAutreFamille', () => { + // Ligne d'une génération : union u (aplomb x=0 à 120), enfant c en x=600 + const at = (id, x, type = 'person', gen = 1) => ({ id, x, width: 120, _type: type, _generation: gen }) + const base = () => [at('u', 0, 'union', 0), at('c', 600), at('sib', 300), at('spouse', 700), at('other', 400)] + const childrenOfUnion = new Map([['u', ['c', 'sib']]]) + const spouseKeysOf = new Map([['c', new Set(['spouse'])]]) + + it('croise quand une personne d\'une autre famille est entre l\'union et l\'enfant', () => { + expect(filCroiseAutreFamille(base(), childrenOfUnion, spouseKeysOf, 'u', 'c')).toBe(true) + }) + + it('ne croise pas quand seuls les frères et sœurs sont entre l\'union et l\'enfant', () => { + const nodes = base().filter(n => n.id !== 'other') + expect(filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, 'u', 'c')).toBe(false) + }) + + it('ne croise pas pour le conjoint de l\'enfant placé du côté de l\'enfant', () => { + const nodes = base().filter(n => n.id !== 'other').map(n => n.id === 'spouse' ? { ...n, x: 300 } : n) + expect(filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, 'u', 'c')).toBe(false) + }) + + it('ignore une personne d\'une autre génération sur la même abscisse', () => { + const nodes = base().filter(n => n.id !== 'other').concat(at('x', 400, 'person', 2)) + expect(filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, 'u', 'c')).toBe(false) + }) +}) diff --git a/frontend/src/components/galaxy/constants.js b/frontend/src/components/galaxy/constants.js index b4c161f..5a90fdc 100644 --- a/frontend/src/components/galaxy/constants.js +++ b/frontend/src/components/galaxy/constants.js @@ -16,8 +16,6 @@ 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 -// Filiation qui traverse l'arbre (enfant placé sous l'autre lignée) : renvoi cliquable au lieu du trait -export const RENVOI_MIN_DX = 360 export const MAX_ORBIT_MEDIAS = 3 export const ORBIT_RADIUS = 55 export const ORBIT_MEDIA_SIZE = 22