/** * FEAT-4: Aufbau und Erweiterung des Ahnenbaums + Konvertierung nach * react-d3-tree. * * Clientseitige Traversierung über die vorhandenen Endpunkte: * Tier → sein Wurf (litterId) → Vater/Mutter → rekursiv, tiefenbegrenzt * (Standard 4 Generationen, Board-Entscheidung). Tiefere Generationen werden * auf Wunsch nachgeladen (expandPedigree). * * Zyklenschutz: Steht ein Tier bereits auf dem Pfad zur Wurzel (defekte Daten: * „eigener Vorfahre“), wird die Rekursion dort beendet und der Knoten nicht * erweiterbar markiert. */ import type { RawNodeDatum } from 'react-d3-tree' import type { Gerbil } from '../api/types' import type { AnimalNode, PedigreeNode, PedigreePath, PedigreeSource, UnknownNode } from './types' /** Standard-Generationstiefe der Erstanzeige (Board: UI 4 Generationen). */ export const DEFAULT_GENERATIONS = 4 function unknown(path: PedigreePath): UnknownNode { return { kind: 'unknown', path } } async function parentNode( source: PedigreeSource, id: string | null | undefined, path: PedigreePath, generationsLeft: number, ancestors: ReadonlySet, ): Promise { if (!id) return unknown(path) const gerbil = await source.getGerbil(id) if (!gerbil) return unknown(path) return buildAnimal(source, gerbil, path, generationsLeft, ancestors) } async function buildAnimal( source: PedigreeSource, gerbil: Gerbil, path: PedigreePath, generationsLeft: number, ancestors: ReadonlySet, ): Promise { const isCycle = ancestors.has(gerbil.id) if (isCycle || generationsLeft <= 0) { // Ladegrenze (oder Zyklus): Eltern nicht laden. Erweiterbar nur, wenn es // einen Wurf gibt und kein Zyklus vorliegt. return { kind: 'animal', path, gerbil, expandable: !isCycle && gerbil.litterId != null } } if (!gerbil.litterId) { // Gründertier ohne erfassten Wurf: Eltern unbekannt. Platzhalter wahren // die Symmetrie der Ahnentafel. return { kind: 'animal', path, gerbil, father: unknown(path + 'v'), mother: unknown(path + 'm'), expandable: false, } } // Wurf laden; bei 404 (gelöschter Wurf) zählen die Eltern als unbekannt. const litter = await source.getLitter(gerbil.litterId) const nextAncestors = new Set(ancestors) nextAncestors.add(gerbil.id) const [father, mother] = await Promise.all([ parentNode(source, litter?.fatherId, path + 'v', generationsLeft - 1, nextAncestors), parentNode(source, litter?.motherId, path + 'm', generationsLeft - 1, nextAncestors), ]) return { kind: 'animal', path, gerbil, father, mother, expandable: false } } /** * Baut den Ahnenbaum für ein Wurzeltier. `null`, wenn das Tier selbst nicht * existiert (404). */ export async function buildPedigree( source: PedigreeSource, rootId: string, generations: number = DEFAULT_GENERATIONS, ): Promise { const root = await source.getGerbil(rootId) if (!root) return null return buildAnimal(source, root, '', generations, new Set()) } /** * Lädt für den Knoten an `path` weitere `generations` Vorfahren-Generationen * nach und liefert eine NEUE Wurzel (unveränderte Äste werden strukturell * geteilt — geeignet für React-State). */ export async function expandPedigree( source: PedigreeSource, root: AnimalNode, path: PedigreePath, generations = 1, ): Promise { const next = await expandAt(source, root, path, new Set(), generations) return next as AnimalNode } async function expandAt( source: PedigreeSource, node: PedigreeNode, target: PedigreePath, ancestors: ReadonlySet, generations: number, ): Promise { if (node.kind === 'unknown') return node if (node.path === target) { return buildAnimal(source, node.gerbil, node.path, generations, ancestors) } if (!target.startsWith(node.path)) return node const branch = target.charAt(node.path.length) // 'v' | 'm' const nextAncestors = new Set(ancestors) nextAncestors.add(node.gerbil.id) if (branch === 'v' && node.father) { return { ...node, father: await expandAt(source, node.father, target, nextAncestors, generations) } } if (branch === 'm' && node.mother) { return { ...node, mother: await expandAt(source, node.mother, target, nextAncestors, generations) } } return node } /* ── Konvertierung für react-d3-tree ──────────────────────────── */ /** * Konvertiert den Ahnenbaum in die react-d3-tree-Struktur. Kinder eines * Knotens sind [Vater, Mutter] (Vater oben — klassische Ahnentafel-Ordnung). * Die Karten-Renderfunktion findet den vollen Knoten über `attributes.path` * (siehe collectNodes); RawNodeDatum.attributes erlaubt nur flache Primitive. */ export function toRawNodeDatum(node: PedigreeNode, unknownLabel: string): RawNodeDatum { if (node.kind === 'unknown') { return { name: unknownLabel, attributes: { path: node.path, kind: 'unknown' } } } const datum: RawNodeDatum = { name: node.gerbil.name, attributes: { path: node.path, kind: 'animal', expandable: node.expandable }, } if (node.father && node.mother) { datum.children = [ toRawNodeDatum(node.father, unknownLabel), toRawNodeDatum(node.mother, unknownLabel), ] } return datum } /** Index aller Knoten nach Position (für die Karten-Renderfunktion). */ export function collectNodes(root: PedigreeNode): Map { const map = new Map() const walk = (node: PedigreeNode): void => { map.set(node.path, node) if (node.kind === 'animal') { if (node.father) walk(node.father) if (node.mother) walk(node.mother) } } walk(root) return map } /** * Alle Plätze einer Generation in Ahnentafel-Ordnung (Vater-Linie zuerst). * Länge = 2^generation; `null` = Platz nicht belegt/geladen (Druckansicht * rendert dafür leere Zellen). */ export function ancestorsAt(root: PedigreeNode, generation: number): (PedigreeNode | null)[] { let slots: (PedigreeNode | null)[] = [root] for (let g = 0; g < generation; g++) { slots = slots.flatMap((node) => node && node.kind === 'animal' ? [node.father ?? null, node.mother ?? null] : [null, null], ) } return slots }