Merge pull request #105 from IliesAllali/fix/placement-trois-correctifs

Placement : rangées stables, couples tournés vers leurs familles, fil libre malgré les conjoints de la fratrie
This commit is contained in:
IliesAllali authored and GitHub committed 2026-10-20 13:54:58 +02:00
commit b057733f64
4 files changed
+84 -3

No files matched your search

+24 -2
View File
@@ -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))
@@ -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')))
})
})
+5 -1
View File
@@ -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
}
// ----------------------------------------------------------
@@ -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)
})
})