/
nek5132
/
Genius
Обзор
Документация
Войти
/
nek5132
/
Genius
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/layout.test.js
403 строки
20 KB
Peter Kosov
feat(rotation): implement hub-centered connected-component rotation; include ancestors + spouses and their descendants; exclude hub descendants; live-edge routing; add debug logs; update tests and docs
14 апр 2026, 12:11
14 апр 2026, 12:11
a241f08
Код
Авторство
О чём код?
import { describe, it, expect } from '@jest/globals'; import { LayoutEngine } from '../src/core/LayoutEngine.js'; describe('LayoutEngine', () => { const engine = new LayoutEngine(); it('places husband left of hub', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [], turned: false } } ], relations: [] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const husband = scene.nodes.find(n => n.id === 'p1'); const hub = scene.nodes.find(n => n.id === 'hub-f1'); expect(husband.position.x).toBeLessThan(hub.position.x); }); it('places wife right of hub', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [], turned: false } } ], relations: [] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const wife = scene.nodes.find(n => n.id === 'p2'); const hub = scene.nodes.find(n => n.id === 'hub-f1'); expect(wife.position.x).toBeGreaterThan(hub.position.x); }); it('generations increase by Y', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male', birthYear: 1950 } }, { id: 'p2', kind: 'person', attrs: { gender: 'male', birthYear: 1975 } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1'], attrs: { husbandId: 'p1', wifeId: null, childrenIds: ['p2'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p2'], attrs: { husbandId: 'p2', wifeId: null, childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p2', childFamilyId: 'f2', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const gen0 = scene.nodes.find(n => n.id === 'p1'); const gen1 = scene.nodes.find(n => n.id === 'p2'); // Descendants go ABOVE (positive Y), ancestors go BELOW (negative Y) expect(gen1.position.y).toBeGreaterThan(gen0.position.y); }); it('child edges are orthogonal', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'male' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1'], attrs: { husbandId: 'p1', wifeId: null, childrenIds: ['p2'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p2'], attrs: { husbandId: 'p2', wifeId: null, childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p2', childFamilyId: 'f2', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const edge = scene.edges.find(e => e.role === 'parent-child'); expect(edge).toBeDefined(); const points = edge.route.points; for (let i = 0; i < points.length - 1; i++) { const dx = Math.abs(points[i+1].x - points[i].x); const dy = Math.abs(points[i+1].y - points[i].y); const dz = Math.abs(points[i+1].z - points[i].z); expect(dx === 0 || dy === 0 || dz === 0).toBe(true); } }); it('parent-child edges have at most 4 segments', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'male' } }, { id: 'p3', kind: 'person', attrs: { gender: 'male' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2', 'p3'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: ['p3'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p3'], attrs: { husbandId: 'p3', wifeId: null, childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p3', childFamilyId: 'f2', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const edge = scene.edges.find(e => e.role === 'parent-child'); expect(edge).toBeDefined(); expect(edge.route.points.length).toBeLessThanOrEqual(5); }); it('edge segments are axis-aligned (no diagonals)', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'male' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1'], attrs: { husbandId: 'p1', wifeId: null, childrenIds: ['p2'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p2'], attrs: { husbandId: 'p2', wifeId: null, childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p2', childFamilyId: 'f2', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const edge = scene.edges.find(e => e.role === 'parent-child'); const points = edge.route.points; for (let i = 0; i < points.length - 1; i++) { const dx = points[i+1].x - points[i].x; const dy = points[i+1].y - points[i].y; const dz = points[i+1].z - points[i].z; const movingAxes = [Math.abs(dx) > 0.01, Math.abs(dy) > 0.01, Math.abs(dz) > 0.01].filter(Boolean).length; expect(movingAxes).toBeLessThanOrEqual(1); } }); it('complex family edges stay orthogonal', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } }, { id: 'p3', kind: 'person', attrs: { gender: 'male' } }, { id: 'p4', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2', 'p3'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: ['p3'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p3', 'p4'], attrs: { husbandId: 'p3', wifeId: 'p4', childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p3', childFamilyId: 'f2', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const edge = scene.edges.find(e => e.role === 'parent-child'); expect(edge).toBeDefined(); const points = edge.route.points; for (let i = 0; i < points.length - 1; i++) { const dx = points[i+1].x - points[i].x; const dy = points[i+1].y - points[i].y; const dz = points[i+1].z - points[i].z; const movingAxes = [Math.abs(dx) > 0.01, Math.abs(dy) > 0.01, Math.abs(dz) > 0.01].filter(Boolean).length; expect(movingAxes).toBeLessThanOrEqual(1); } }); it('radial layout generates nodes', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'radial-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [], turned: false } } ], relations: [] }; const scene = engine.compute(state, '3d', 'radial-3d'); expect(scene.nodes.length).toBeGreaterThan(0); expect(scene.layout).toBe('radial-3d'); }); it('radial layout with children spreads radially', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'radial-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'male' } }, { id: 'p3', kind: 'person', attrs: { gender: 'male' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1'], attrs: { husbandId: 'p1', wifeId: null, childrenIds: ['p2', 'p3'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p2'], attrs: { husbandId: 'p2', wifeId: null, childrenIds: [], turned: false } }, { id: 'f3', kind: 'family', memberIds: ['p3'], attrs: { husbandId: 'p3', wifeId: null, childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p2', childFamilyId: 'f2', roleInChildFamily: 'husband' } }, { id: 'r2', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f3', attrs: { parentFamilyId: 'f1', childPersonId: 'p3', childFamilyId: 'f3', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'radial-3d'); const hubF1 = scene.nodes.find(n => n.id === 'hub-f1'); const childNodes = scene.nodes.filter(n => n.id !== 'hub-f1' && n.role === 'person' && n.position.y < hubF1.position.y); expect(childNodes.length).toBeGreaterThanOrEqual(2); const angles = childNodes.map(n => Math.atan2(n.position.z, n.position.x)); const angleDiff = Math.abs(angles[0] - angles[1]); expect(angleDiff).toBeGreaterThan(0.1); }); it('radial layout supports multiple roots', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'radial-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } }, { id: 'p3', kind: 'person', attrs: { gender: 'male' } }, { id: 'p4', kind: 'person', attrs: { gender: 'female' } }, { id: 'p5', kind: 'person', attrs: { gender: 'male' } }, { id: 'p6', kind: 'person', attrs: { gender: 'female' } }, { id: 'p7', kind: 'person', attrs: { gender: 'male' } }, { id: 'p8', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2', 'p3'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: ['p3'], turned: false } }, { id: 'f2', kind: 'family', memberIds: ['p3', 'p4'], attrs: { husbandId: 'p3', wifeId: 'p4', childrenIds: [], turned: false } }, { id: 'f3', kind: 'family', memberIds: ['p5', 'p6', 'p7'], attrs: { husbandId: 'p5', wifeId: 'p6', childrenIds: ['p7'], turned: false } }, { id: 'f4', kind: 'family', memberIds: ['p7', 'p8'], attrs: { husbandId: 'p7', wifeId: 'p8', childrenIds: [], turned: false } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', fromId: 'f1', toId: 'f2', attrs: { parentFamilyId: 'f1', childPersonId: 'p3', childFamilyId: 'f2', roleInChildFamily: 'husband' } }, { id: 'r2', kind: 'parent-child-family-link', fromId: 'f3', toId: 'f4', attrs: { parentFamilyId: 'f3', childPersonId: 'p7', childFamilyId: 'f4', roleInChildFamily: 'husband' } } ] }; const scene = engine.compute(state, '3d', 'radial-3d'); const hubs = scene.nodes.filter(n => n.role === 'family_hub'); expect(hubs.length).toBe(4); const rootHubs = hubs.filter(h => h.position.y === 0); expect(rootHubs.length).toBe(2); const dist = Math.hypot(rootHubs[0].position.x - rootHubs[1].position.x, rootHubs[0].position.z - rootHubs[1].position.z); expect(dist).toBeGreaterThan(100); }); it('layout includes bounds', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [], turned: false } } ], relations: [] }; const scene = engine.compute(state, '3d', 'vertical-3d'); expect(scene.bounds).toBeDefined(); expect(scene.bounds.min).toBeDefined(); expect(scene.bounds.max).toBeDefined(); }); it('both spouses have edges to hub', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male' } }, { id: 'p2', kind: 'person', attrs: { gender: 'female' } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [], turned: false } } ], relations: [] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const spouseEdges = scene.edges.filter(e => e.role === 'spouse'); expect(spouseEdges.length).toBe(2); const husbandEdge = spouseEdges.find(e => e.fromId === 'p1'); const wifeEdge = spouseEdges.find(e => e.fromId === 'p2'); expect(husbandEdge).toBeDefined(); expect(wifeEdge).toBeDefined(); expect(husbandEdge.toId).toBe('hub-f1'); expect(wifeEdge.toId).toBe('hub-f1'); }); it('ancestor families with birthYear < 1900 are not root families', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male', birthYear: 1940 } }, { id: 'p2', kind: 'person', attrs: { gender: 'female', birthYear: 1945 } }, { id: 'p3', kind: 'person', attrs: { gender: 'male', birthYear: 1870 } }, { id: 'p4', kind: 'person', attrs: { gender: 'female', birthYear: 1875 } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: ['p3'] } }, { id: 'f2', kind: 'family', memberIds: ['p3', 'p4'], attrs: { husbandId: 'p3', wifeId: 'p4', childrenIds: [] } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', attrs: { parentFamilyId: 'f1', childFamilyId: 'f2', childPersonId: 'p3' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const rootHubs = scene.nodes.filter(n => n.role === 'family_hub' && n.position.y === 0); expect(rootHubs.length).toBe(1); expect(rootHubs[0].groupId).toBe('f1'); const allHubs = scene.nodes.filter(n => n.role === 'family_hub'); expect(allHubs.length).toBe(2); }); it('every family_hub node has exactly one spouse on each side', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male', birthYear: 1940 } }, { id: 'p2', kind: 'person', attrs: { gender: 'female', birthYear: 1942 } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [] } } ], relations: [] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const hub = scene.nodes.find(n => n.role === 'family_hub' && n.groupId === 'f1'); expect(hub).toBeDefined(); const hubId = hub.id; const spouseEdges = scene.edges.filter(e => e.role === 'spouse' && e.toId === hubId); expect(spouseEdges.length).toBe(2); const husbandEdge = spouseEdges.find(e => e.fromId === 'p1'); const wifeEdge = spouseEdges.find(e => e.fromId === 'p2'); expect(husbandEdge).toBeDefined(); expect(wifeEdge).toBeDefined(); }); it('no person nodes have duplicate positions within same family', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male', birthYear: 1940 } }, { id: 'p2', kind: 'person', attrs: { gender: 'female', birthYear: 1942 } }, { id: 'p3', kind: 'person', attrs: { gender: 'male', birthYear: 1965 } }, { id: 'p4', kind: 'person', attrs: { gender: 'female', birthYear: 1968 } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2', 'p3'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: ['p3'] } }, { id: 'f2', kind: 'family', memberIds: ['p3', 'p4'], attrs: { husbandId: 'p3', wifeId: 'p4', childrenIds: [] } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', attrs: { parentFamilyId: 'f1', childFamilyId: 'f2', childPersonId: 'p3' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const personNodes = scene.nodes.filter(n => n.role === 'person'); for (let i = 0; i < personNodes.length; i++) { for (let j = i + 1; j < personNodes.length; j++) { const pos1 = personNodes[i].position; const pos2 = personNodes[j].position; const samePosition = pos1.x === pos2.x && pos1.y === pos2.y && pos1.z === pos2.z; if (samePosition) { console.log('DUPLICATE POSITION:', personNodes[i].id, 'and', personNodes[j].id, 'at', pos1); } expect(samePosition).toBe(false); } } }); it('ancestor families are placed below root families (lower Y)', () => { const state = { config: { modelProfile: 'genealogy', layoutVersion: 'ortho-tree-v1', styleVersion: 'white-gray-v1' }, entities: [ { id: 'p1', kind: 'person', attrs: { gender: 'male', birthYear: 1940 } }, { id: 'p2', kind: 'person', attrs: { gender: 'female', birthYear: 1942 } }, { id: 'p3', kind: 'person', attrs: { gender: 'male', birthYear: 1870 } }, { id: 'p4', kind: 'person', attrs: { gender: 'female', birthYear: 1875 } }, { id: 'p7', kind: 'person', attrs: { gender: 'male', birthYear: 1860 } }, { id: 'p8', kind: 'person', attrs: { gender: 'female', birthYear: 1865 } } ], groups: [ { id: 'f1', kind: 'family', memberIds: ['p1', 'p2'], attrs: { husbandId: 'p1', wifeId: 'p2', childrenIds: [] } }, { id: 'f2', kind: 'family', memberIds: ['p3', 'p4', 'p1'], attrs: { husbandId: 'p3', wifeId: 'p4', childrenIds: ['p1'] } }, { id: 'f3', kind: 'family', memberIds: ['p7', 'p8', 'p3'], attrs: { husbandId: 'p7', wifeId: 'p8', childrenIds: ['p3'] } } ], relations: [ { id: 'r1', kind: 'parent-child-family-link', attrs: { parentFamilyId: 'f2', childFamilyId: 'f1', childPersonId: 'p1' } }, { id: 'r2', kind: 'parent-child-family-link', attrs: { parentFamilyId: 'f3', childFamilyId: 'f2', childPersonId: 'p3' } } ] }; const scene = engine.compute(state, '3d', 'vertical-3d'); const f1Hub = scene.nodes.find(n => n.role === 'family_hub' && n.groupId === 'f1'); const f2Hub = scene.nodes.find(n => n.role === 'family_hub' && n.groupId === 'f2'); const f3Hub = scene.nodes.find(n => n.role === 'family_hub' && n.groupId === 'f3'); expect(f1Hub).toBeDefined(); expect(f2Hub).toBeDefined(); expect(f3Hub).toBeDefined(); // f1 is root (no parent), f2 is ancestor of f1, f3 is ancestor of f2 // Ancestors should be below root: f2.y < f1.y and f3.y < f2.y expect(f2Hub.position.y).toBeLessThan(f1Hub.position.y); expect(f3Hub.position.y).toBeLessThan(f2Hub.position.y); }); });