mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-12 16:16:09 +00:00
Filiations : accolade franche (forme A)
Remplace le virage quadratique par une accolade à coins arrondis de rayon fixe (24 px) : un tronc depuis l'union, une barre de fratrie, puis une descente vers chaque enfant. Un enfant à l'aplomb de l'union descend droit. Le chemin surligné (sélection, survol) passe par la même géométrie que la fratrie. Tests unitaires de traceSibship (un chemin et un stroke par fratrie, même géométrie pour un enfant seul, pas de virage à l'aplomb). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d58b7b779c
commit
9d9e375f58
2 files changed
+57
-54
No files matched your search
@@ -91,15 +91,14 @@ function computeCouplePoints(p1Pos, p2Pos, unionPos, p1R, p2R, nodeRandomData, s
|
||||
// Fil : terracotta clair, union en rond terracotta (le rond du logo).
|
||||
// Filiation en accolade souple : l'union descend vers une ligne
|
||||
// commune à la fratrie, chaque enfant s'en détache par une courbe.
|
||||
// Les virages sont des quadratiques larges, pas des coins.
|
||||
// Coins arrondis de rayon fixe (accolade franche, forme A).
|
||||
// ============================================================
|
||||
export const FIL = { color: '#E3A590', width: 1.1, dot: '#D2694A', dotR: 2.6 }
|
||||
// Ligne de la fratrie : sous les prénoms et années des parents, au-dessus des enfants
|
||||
const BUS_DY = 100
|
||||
// Rayon horizontal des virages (plafonné à la moitié de l'écart union → enfant)
|
||||
const BUS_TURN = 56
|
||||
// En dessous de cet écart union → enfant, pas de virage
|
||||
const BUS_STRAIGHT_DX = 30
|
||||
// Accolade : coins arrondis de rayon fixe (forme A, choisie par Patch), écart minimal en dessous duquel l'enfant descend droit
|
||||
const SIB_R = 24
|
||||
const SIB_STRAIGHT_DX = 12
|
||||
|
||||
function busY(sourceId, sy) {
|
||||
let h = 0
|
||||
@@ -108,65 +107,29 @@ function busY(sourceId, sy) {
|
||||
return sy + BUS_DY + (Math.abs(h) % 3) * 4
|
||||
}
|
||||
|
||||
function traceBus(ctx, sx, sy, by, ex, ey) {
|
||||
const dx = ex - sx
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(sx, sy)
|
||||
// Enfant presque sous l'union (enfant unique, décalage aléatoire de quelques px) : trait direct, pas de S
|
||||
if (Math.abs(dx) < BUS_STRAIGHT_DX) { ctx.lineTo(ex, ey); ctx.stroke(); return }
|
||||
const dir = Math.sign(dx)
|
||||
const turn = Math.min(BUS_TURN, Math.abs(dx) / 2)
|
||||
// Descente de l'union, virage large vers la ligne de la fratrie
|
||||
const drop = Math.min(by - sy, 64)
|
||||
ctx.lineTo(sx, by - drop)
|
||||
ctx.quadraticCurveTo(sx, by, sx + dir * turn, by)
|
||||
// Ligne de la fratrie, puis courbe jusqu'au portrait de l'enfant
|
||||
ctx.lineTo(ex - dir * turn, by)
|
||||
ctx.quadraticCurveTo(ex, by, ex, ey)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Une union, toute sa fratrie : une seule trajectoire (un tronc, une accolade, un trait par enfant).
|
||||
// Un chemin par enfant aurait un rayon de virage propre, donc des brins qui se séparent sous l'union.
|
||||
// Le rayon est commun à chaque côté (le plus petit écart du côté), et le tout part dans un seul stroke
|
||||
// pour que les parties superposées ne s'additionnent pas en alpha.
|
||||
function traceSibship(ctx, sourceId, sourcePos, items) {
|
||||
// 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) {
|
||||
const sx = sourcePos.cx, sy = sourcePos.cy
|
||||
const by = busY(sourceId, sy)
|
||||
const drop = Math.min(by - sy, 64)
|
||||
const turnFor = (dir) => {
|
||||
let t = BUS_TURN
|
||||
for (const it of items) {
|
||||
const dx = it.cx - sx
|
||||
if (Math.sign(dx) === dir && Math.abs(dx) >= BUS_STRAIGHT_DX) t = Math.min(t, Math.abs(dx) / 2)
|
||||
}
|
||||
return t
|
||||
}
|
||||
const turns = { '-1': turnFor(-1), '1': turnFor(1) }
|
||||
const rr = Math.min(SIB_R, by - sy - 2)
|
||||
ctx.beginPath()
|
||||
for (const { cx: ex, cy: ey } of items) {
|
||||
const dx = ex - sx
|
||||
ctx.moveTo(sx, sy)
|
||||
ctx.lineTo(sx, by - drop)
|
||||
// Enfant presque sous l'union : descente droite depuis la barre, pas de second brin depuis l'union
|
||||
if (Math.abs(dx) < BUS_STRAIGHT_DX) { ctx.lineTo(ex, by); ctx.lineTo(ex, ey); continue }
|
||||
if (Math.abs(dx) < SIB_STRAIGHT_DX) { ctx.lineTo(sx, by); ctx.lineTo(ex, by); ctx.lineTo(ex, ey); continue }
|
||||
const dir = Math.sign(dx)
|
||||
const turn = turns[dir]
|
||||
ctx.quadraticCurveTo(sx, by, sx + dir * turn, by)
|
||||
ctx.lineTo(ex - dir * turn, by)
|
||||
ctx.quadraticCurveTo(ex, by, ex, ey)
|
||||
const k = Math.min(rr, Math.abs(dx) / 2)
|
||||
ctx.lineTo(sx, by - k)
|
||||
ctx.arcTo(sx, by, sx + dir * k, by, k)
|
||||
ctx.lineTo(ex - dir * k, by)
|
||||
ctx.arcTo(ex, by, ex, ey, k)
|
||||
ctx.lineTo(ex, ey)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Trace une filiation union → enfant. progress < 1 : entrée en fondu
|
||||
function traceFiliation(ctx, sourceId, sourcePos, targetPos, childR, progress) {
|
||||
const a = ctx.globalAlpha
|
||||
ctx.globalAlpha = a * progress
|
||||
traceBus(ctx, sourcePos.cx, sourcePos.cy, busY(sourceId, sourcePos.cy), targetPos.cx, targetPos.cy - childR)
|
||||
ctx.globalAlpha = a
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Courbes de couple (Bézier)
|
||||
// ============================================================
|
||||
@@ -411,7 +374,9 @@ export function drawPathHighlight(ctx, {
|
||||
ctx.lineCap = 'round'
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
traceFiliation(ctx, edge.sources[0], sourcePos, targetPos, childR, hp)
|
||||
ctx.globalAlpha = hp
|
||||
traceSibship(ctx, edge.sources[0], sourcePos, [{ cx: targetPos.cx, cy: targetPos.cy - childR }])
|
||||
ctx.globalAlpha = 1
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { traceSibship } from './renderers'
|
||||
|
||||
// Contexte canvas minimal qui enregistre les appels de tracé
|
||||
const recorder = () => {
|
||||
const calls = []
|
||||
const ctx = new Proxy({}, { get: (_, name) => (...args) => { calls.push([name, ...args]) } })
|
||||
return { ctx, calls, count: (n) => calls.filter(c => c[0] === n).length }
|
||||
}
|
||||
const src = { cx: 500, cy: 300 }
|
||||
|
||||
describe('traceSibship (accolade franche)', () => {
|
||||
it('trace toute la fratrie en un seul chemin et un seul stroke', () => {
|
||||
const { ctx, count } = recorder()
|
||||
traceSibship(ctx, 'u-1', src, [{ cx: 200, cy: 480 }, { cx: 420, cy: 480 }, { cx: 800, cy: 480 }])
|
||||
expect(count('beginPath')).toBe(1)
|
||||
expect(count('stroke')).toBe(1)
|
||||
expect(count('moveTo')).toBe(3)
|
||||
})
|
||||
|
||||
it('un enfant seul rejoint la barre avec la même géométrie que dans la fratrie', () => {
|
||||
const child = { cx: 800, cy: 480 }
|
||||
const solo = recorder()
|
||||
traceSibship(solo.ctx, 'u-1', src, [child])
|
||||
const group = recorder()
|
||||
traceSibship(group.ctx, 'u-1', src, [child, { cx: 200, cy: 480 }])
|
||||
const soloArcs = solo.calls.filter(c => c[0] === 'arcTo')
|
||||
const groupArcs = group.calls.filter(c => c[0] === 'arcTo').slice(0, 2)
|
||||
expect(soloArcs.length).toBe(2)
|
||||
expect(groupArcs).toEqual(soloArcs)
|
||||
})
|
||||
|
||||
it('un enfant à l\'aplomb de l\'union descend droit, sans virage', () => {
|
||||
const { ctx, count } = recorder()
|
||||
traceSibship(ctx, 'u-1', src, [{ cx: 505, cy: 480 }])
|
||||
expect(count('arcTo')).toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user