diff --git a/frontend/src/components/galaxy/blockLayout.js b/frontend/src/components/galaxy/blockLayout.js index c20275b..055e797 100644 --- a/frontend/src/components/galaxy/blockLayout.js +++ b/frontend/src/components/galaxy/blockLayout.js @@ -27,12 +27,14 @@ 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. +// 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 +// (un petit conjoint collé à un frère faisait passer le fil en pastille). 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 fratrie = [...new Set([c, ...(childrenOfUnion.get(u) || [])])] + const excluded = new Set([...fratrie, ...fratrie.flatMap(k => [...(spouseKeysOf.get(k) || [])])]) 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) @@ -215,6 +217,26 @@ export function computeBlockLayout(graphData = null) { if (!found) { block.place(cursor); cursor += block.width + FAMILY_GAP * 2 } } + // Couple dont les deux conjoints ont leurs parents dans l'arbre, chacun du côté de la famille de l'autre : + // on intervertit les deux conjoints (même place, l'union reste au milieu), les deux fils ne se croisent plus. + // Répété : intervertir un couple peut remettre à l'endroit ou à l'envers celui de ses enfants. + const centerOf = (id) => pos.get(id) + byId.get(id).width / 2 + for (let pass = 0; pass < 4; pass++) { + let swapped = 0 + for (const chain of chains) { + if (chain.length !== 3 || !pos.has(chain[0])) continue + const [a, , b] = chain + const pa = parentUnionOf.get(a), pb = parentUnionOf.get(b) + if (!pa || !pb || pa === pb || !pos.has(pa) || !pos.has(pb)) continue + if ((centerOf(a) - centerOf(b)) * (centerOf(pa) - centerOf(pb)) >= 0) continue + const xa = pos.get(a), xb = pos.get(b) + pos.set(a, xb + byId.get(b).width - byId.get(a).width) + pos.set(b, xa) + swapped++ + } + if (!swapped) break + } + // Réinjecter le X, normalisé à 0 ; Y inchangé (générations) let minX = Infinity for (const n of nodes) if (pos.has(n.id)) minX = Math.min(minX, pos.get(n.id)) diff --git a/frontend/src/components/galaxy/blockLayout.test.js b/frontend/src/components/galaxy/blockLayout.test.js index 80e1148..1cc18b1 100644 --- a/frontend/src/components/galaxy/blockLayout.test.js +++ b/frontend/src/components/galaxy/blockLayout.test.js @@ -102,8 +102,35 @@ describe('filCroiseAutreFamille', () => { expect(filCroiseAutreFamille(nodes, childrenOfUnion, spouseKeysOf, 'u', 'c')).toBe(false) }) + it('ne croise pas pour le conjoint d\'un frère ou d\'une sœur', () => { + const nodes = base().filter(n => n.id !== 'other').concat(at('beaufrere', 420)) + const spouses = new Map([...spouseKeysOf, ['sib', new Set(['beaufrere'])]]) + expect(filCroiseAutreFamille(nodes, childrenOfUnion, spouses, '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) }) }) + +describe('computeBlockLayout, couple tourné vers les familles de chacun', () => { + // Marc (fratrie de Louis et Hélène) épouse Nadia (fratrie de Pierre et Odile). La fratrie de Nadia est saisie + // en premier, Marc avant Nadia : Marc se retrouvait à gauche du couple alors que sa famille est à droite, + // et les deux fils se croisaient. + const graph = { + persons: ['pierre', 'odile', 'louis', 'helene', 'marc', 'claire', 'julien', 'sophie', 'eric', 'luc', 'paul', 'mireille', 'nadia', 'anais'].map(person), + unions: [union('uPO', 'pierre', 'odile'), { ...union('uLH', 'louis', 'helene'), displayOrder: 2 }, { ...union('uMN', 'marc', 'nadia'), displayOrder: 3 }], + filiations: [ + ...['luc', 'paul', 'mireille', 'nadia'].map((k, i) => ({ ...fil(`f-${k}`, k, 'uPO'), displayOrder: i + 1 })), + ...['marc', 'claire', 'julien', 'sophie', 'eric'].map((k, i) => ({ ...fil(`f-${k}`, k, 'uLH'), displayOrder: i + 1 })), + fil('f-anais', 'anais', 'uMN'), + ], + } + + it('place chaque conjoint du côté de ses propres parents', () => { + const { children } = computeBlockLayout(graph) + const x = (id) => { const n = children.find(c => c.id === id); return n.x + n.width / 2 } + expect(Math.sign(x('p-marc') - x('p-nadia'))).toBe(Math.sign(x('u-uLH') - x('u-uPO'))) + }) +}) diff --git a/frontend/src/components/galaxy/elkLayout.js b/frontend/src/components/galaxy/elkLayout.js index 9151b81..4a2c3ec 100644 --- a/frontend/src/components/galaxy/elkLayout.js +++ b/frontend/src/components/galaxy/elkLayout.js @@ -258,7 +258,10 @@ export function computeLayout(graphData = null) { // Ajuster les racines orphelines : si un ancêtre sans parents connus // a des enfants à gen N, il doit être à gen N-1 (pas gen 0). - { + // Répété jusqu'à stabilité : un enfant descendu par son conjoint fait à son tour descendre ses parents + // sans ascendance (sinon ils restent en haut, et l'enfant pend plusieurs rangées sous sa fratrie). + for (let pass = 0; pass < 12; pass++) { + const before = persons.map(p => genMap.get(p.id)).join() const rootIds = new Set(persons.filter(p => !filiationsByChild.has(p.id)).map(p => p.id)) const maxChildGenByRoot = new Map() normalizedFiliations.forEach(f => { @@ -298,6 +301,7 @@ export function computeLayout(graphData = null) { genMap.set(u.partner2Id, maxG) }) } + if (persons.map(p => genMap.get(p.id)).join() === before) break } // ---------------------------------------------------------- diff --git a/frontend/src/components/galaxy/elkLayout.test.js b/frontend/src/components/galaxy/elkLayout.test.js index dee8f93..f956604 100644 --- a/frontend/src/components/galaxy/elkLayout.test.js +++ b/frontend/src/components/galaxy/elkLayout.test.js @@ -267,3 +267,31 @@ describe('separated parents', () => { expect(separatedPairUnions([filiations[0]], [])).toEqual([]) }) }) + +describe('rangées des ancêtres sans parents', () => { + // Henri + Isabelle (sans ascendance) ont Joseph et Nicolas. Nicolas épouse Madeleine (sans ascendance) ; + // leur fille Anne épouse Jean, d'une lignée qui remonte sur cinq générations. Anne descend au niveau de Jean, + // Nicolas avec elle : ses parents et son frère doivent suivre, au lieu de rester tout en haut. + const ids = ['henri', 'isabelle', 'joseph', 'nicolas', 'madeleine', 'anne', 'jean', 'l0', 'l0w', 'l1', 'l1w', 'l2', 'l2w', 'l3', 'l3w', 'l4', 'l4w'] + const persons = ids.map((id) => ({ id })) + const unions = [ + { id: 'uHI', partner1Id: 'henri', partner2Id: 'isabelle', displayOrder: 1 }, + { id: 'uNM', partner1Id: 'nicolas', partner2Id: 'madeleine', displayOrder: 2 }, + { id: 'uAJ', partner1Id: 'anne', partner2Id: 'jean', displayOrder: 3 }, + ...[0, 1, 2, 3, 4].map((i) => ({ id: `uL${i}`, partner1Id: `l${i}`, partner2Id: `l${i}w`, displayOrder: 4 + i })), + ] + const link = (id, childId, unionId) => ({ id, childId, unionId, parentId: null, parentageType: 'biologique', displayOrder: 1 }) + const filiations = [ + link('f1', 'joseph', 'uHI'), link('f2', 'nicolas', 'uHI'), link('f3', 'anne', 'uNM'), + link('f4', 'l1', 'uL0'), link('f5', 'l2', 'uL1'), link('f6', 'l3', 'uL2'), link('f7', 'l4', 'uL3'), link('f8', 'jean', 'uL4'), + ] + + it('fait descendre les parents et la fratrie de celui qui a suivi son conjoint', () => { + const result = computeLayout({ persons, unions, filiations }) + const gen = new Map(result.children.filter((n) => n._type === 'person').map((n) => [n.id.slice(2), n._generation])) + expect(gen.get('anne')).toBe(gen.get('jean')) + expect(gen.get('nicolas')).toBe(gen.get('anne') - 1) + expect(gen.get('joseph')).toBe(gen.get('nicolas')) + expect(gen.get('henri')).toBe(gen.get('nicolas') - 1) + }) +})