mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-10 15:52:14 +00:00
Renvoi seulement si le fil croise une autre famille
Une filiation vers un enfant placé sous l'autre lignée devenait un renvoi (pastilles) dès que l'écart union → enfant dépassait 360 px, même quand le fil ne croisait personne. Le critère est maintenant géométrique : le fil file sur la ligne de la fratrie entre l'aplomb de l'union et l'enfant ; il ne devient renvoi que si une personne d'une autre famille se trouve sur cette ligne. Frères et sœurs et conjoint(e)s de l'enfant ne comptent pas. Tests : cas à fratrie voisine (fil conservé), et filCroiseAutreFamille sur des nœuds synthétiques (autre famille, fratrie seule, conjoint, autre génération). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8d4b4113d
commit
3d709ec015
3 files changed
+76
-8
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user