import { describe, expect, test } from 'bun:test' import type { Mesh } from 'three' import { buildShelfGeometry, shelfRowSurfaceYs } from '../geometry' import { ShelfNode } from '../schema' describe('buildShelfGeometry — wall-shelf', () => { test('returns a Group with one board + two brackets (default v1 shape)', () => { const node = ShelfNode.parse({}) const group = buildShelfGeometry(node) const names = group.children.map((c) => c.name) expect(names).toContain('shelf-board-0') expect(names).toContain('shelf-bracket-left') expect(names).toContain('shelf-bracket-right') expect(group.children.length).toBe(3) }) test('hidden bracket style omits both brackets', () => { const node = ShelfNode.parse({ bracketStyle: 'hidden' }) const group = buildShelfGeometry(node) expect(group.children.length).toBe(1) expect(group.children[0]!.name).toBe('shelf-board-0') }) test('top board y-center matches height + thickness/2 (v1 semantic preserved)', () => { const node = ShelfNode.parse({ height: 1.0, thickness: 0.05 }) const group = buildShelfGeometry(node) const top = group.children.find((c) => c.name === 'shelf-board-0') as Mesh | undefined expect(top).toBeDefined() expect(top!.position.y).toBeCloseTo(1.0 + 0.025) }) test('rows > 1 produces multiple boards evenly spaced from height/rows to height', () => { const node = ShelfNode.parse({ rows: 3, height: 1.8, thickness: 0.04 }) const group = buildShelfGeometry(node) const boards = group.children.filter((c) => c.name.startsWith('shelf-board-')) as Mesh[] expect(boards.length).toBe(3) const ys = boards.map((b) => b.position.y).sort((a, b) => a - b) expect(ys[0]).toBeCloseTo(0.6 + 0.02) expect(ys[1]).toBeCloseTo(1.2 + 0.02) expect(ys[2]).toBeCloseTo(1.8 + 0.02) }) test('industrial bracket style produces wider bracket boxes', () => { const minimal = buildShelfGeometry(ShelfNode.parse({ bracketStyle: 'minimal', depth: 0.4 })) const industrial = buildShelfGeometry( ShelfNode.parse({ bracketStyle: 'industrial', depth: 0.4 }), ) const minimalBracket = minimal.children.find((c) => c.name === 'shelf-bracket-left') as Mesh const industrialBracket = industrial.children.find( (c) => c.name === 'shelf-bracket-left', ) as Mesh const minimalParams = (minimalBracket.geometry as any).parameters const industrialParams = (industrialBracket.geometry as any).parameters expect(industrialParams.width).toBeGreaterThan(minimalParams.width) }) }) describe('buildShelfGeometry — bookshelf', () => { test('emits side panels + multiple boards', () => { const node = ShelfNode.parse({ style: 'bookshelf', rows: 4, height: 1.8 }) const group = buildShelfGeometry(node) const names = group.children.map((c) => c.name) expect(names).toContain('shelf-side-left') expect(names).toContain('shelf-side-right') expect(names.filter((n) => n.startsWith('shelf-board-')).length).toBe(4) }) test('withBack adds a back panel', () => { const without = buildShelfGeometry(ShelfNode.parse({ style: 'bookshelf', withBack: false })) const withBack = buildShelfGeometry(ShelfNode.parse({ style: 'bookshelf', withBack: true })) expect(without.children.find((c) => c.name === 'shelf-back')).toBeUndefined() expect(withBack.children.find((c) => c.name === 'shelf-back')).toBeDefined() }) test('columns > 1 adds vertical dividers', () => { const node = ShelfNode.parse({ style: 'bookshelf', columns: 3 }) const group = buildShelfGeometry(node) const dividers = group.children.filter((c) => c.name.startsWith('shelf-divider-col-')) expect(dividers.length).toBe(2) }) test('withSides=false replaces side panels with corner posts', () => { const node = ShelfNode.parse({ style: 'bookshelf', withSides: false }) const group = buildShelfGeometry(node) const names = group.children.map((c) => c.name) expect(names).not.toContain('shelf-side-left') expect(names.filter((n) => n.startsWith('shelf-post-')).length).toBe(4) }) }) describe('buildShelfGeometry — open-rack', () => { test('always emits four corner posts', () => { const node = ShelfNode.parse({ style: 'open-rack' }) const group = buildShelfGeometry(node) const posts = group.children.filter((c) => c.name.startsWith('shelf-post-')) expect(posts.length).toBe(4) }) test('withBack adds horizontal cross-braces top + bottom', () => { const node = ShelfNode.parse({ style: 'open-rack', withBack: true }) const group = buildShelfGeometry(node) const braces = group.children.filter((c) => c.name.startsWith('shelf-brace-h-')) expect(braces.length).toBe(2) }) }) describe('buildShelfGeometry — cubby', () => { test('grid of cubbies emits sides + back + boards + dividers', () => { const node = ShelfNode.parse({ style: 'cubby', rows: 3, columns: 3 }) const group = buildShelfGeometry(node) const names = group.children.map((c) => c.name) expect(names).toContain('shelf-side-left') expect(names).toContain('shelf-side-right') expect(names).toContain('shelf-back') // Boards: rows = 3 → 3 horizontal boards. expect(names.filter((n) => n.startsWith('shelf-board-')).length).toBe(3) // Dividers: (columns − 1) per row → 2 × 3 = 6. expect(names.filter((n) => /^shelf-divider-\d+-\d+$/.test(n)).length).toBe(6) }) test('withBottom adds a floor board at y = thickness/2', () => { const node = ShelfNode.parse({ style: 'cubby', withBottom: true, thickness: 0.05 }) const group = buildShelfGeometry(node) const bottom = group.children.find((c) => c.name === 'shelf-board-bottom') as Mesh | undefined expect(bottom).toBeDefined() expect(bottom!.position.y).toBeCloseTo(0.025) }) test('withBottom=false omits the floor board', () => { const node = ShelfNode.parse({ style: 'cubby', withBottom: false }) const group = buildShelfGeometry(node) expect(group.children.find((c) => c.name === 'shelf-board-bottom')).toBeUndefined() }) }) describe('shelfRowSurfaceYs — withBottom', () => { test('prepends y = thickness when cubby has withBottom on', () => { const node = ShelfNode.parse({ style: 'cubby', withBottom: true, rows: 3, height: 1.8, thickness: 0.05, }) const ys = shelfRowSurfaceYs(node) expect(ys.length).toBe(4) expect(ys[0]).toBeCloseTo(0.05) // top of bottom board }) test('ignores withBottom for wall-shelf', () => { const node = ShelfNode.parse({ style: 'wall-shelf', withBottom: true }) const ys = shelfRowSurfaceYs(node) expect(ys.length).toBe(1) }) }) describe('shelfRowSurfaceYs', () => { test('returns one Y per row, all at board top', () => { const node = ShelfNode.parse({ rows: 3, height: 1.8, thickness: 0.04 }) const ys = shelfRowSurfaceYs(node) expect(ys.length).toBe(3) // Y values are sorted ascending and represent top-of-board. expect(ys[0]).toBeCloseTo(0.6 + 0.04) expect(ys[1]).toBeCloseTo(1.2 + 0.04) expect(ys[2]).toBeCloseTo(1.8 + 0.04) }) test('rows=1 returns single Y at v1 top-of-board (height + thickness)', () => { const node = ShelfNode.parse({ height: 0.9, thickness: 0.04 }) const ys = shelfRowSurfaceYs(node) expect(ys.length).toBe(1) expect(ys[0]).toBeCloseTo(0.94) }) }) describe('material application', () => { test('default shelf material is the canonical white shared with walls / stairs', () => { const board = buildShelfGeometry(ShelfNode.parse({})).children.find( (c) => c.name === 'shelf-board-0', ) as Mesh const material = board.material as { color: { getHexString(): string } } // DEFAULT_SHELF_MATERIAL is '#ffffff' — same as DEFAULT_WALL_MATERIAL / // DEFAULT_STAIR_MATERIAL so an unpainted shelf reads as the same // "default white" surface the rest of the structural kinds use. expect(material.color.getHexString().toLowerCase()).toBe('ffffff') }) test('user-set material is applied (not the default)', () => { const defaultBoard = ( buildShelfGeometry(ShelfNode.parse({})).children.find( (c) => c.name === 'shelf-board-0', ) as Mesh ).material as { color: { getHexString(): string } } const customBoard = ( buildShelfGeometry( ShelfNode.parse({ material: { properties: { color: '#112233' } }, }), ).children.find((c) => c.name === 'shelf-board-0') as Mesh ).material as { color: { getHexString(): string } } expect(customBoard.color.getHexString()).not.toBe(defaultBoard.color.getHexString()) }) })