184 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, test } from 'bun:test'
import type { AnyNode } from '@pascal-app/core/schema'
import { computeSceneBoundsXZ } from './scene-bounds'
function makeWall(start: [number, number], end: [number, number]): AnyNode {
return {
object: 'node',
id: `wall_${start.join('_')}_${end.join('_')}`,
type: 'wall',
parentId: null,
visible: true,
metadata: {},
children: [],
start,
end,
frontSide: 'unknown',
backSide: 'unknown',
} as unknown as AnyNode
}
function makeZone(polygon: [number, number][]): AnyNode {
return {
object: 'node',
id: `zone_${polygon.length}_${polygon[0]?.[0] ?? 0}`,
type: 'zone',
parentId: null,
visible: true,
metadata: {},
name: 'Zone',
polygon,
color: '#000000',
} as unknown as AnyNode
}
function makeSite(points: [number, number][]): AnyNode {
return {
object: 'node',
id: 'site_test',
type: 'site',
parentId: null,
visible: true,
metadata: {},
polygon: { type: 'polygon', points },
children: [],
} as unknown as AnyNode
}
describe('computeSceneBoundsXZ', () => {
test('returns null when given an empty array', () => {
expect(computeSceneBoundsXZ([])).toBeNull()
})
test('returns null when no geometry is found on any node', () => {
const barren = [
{
object: 'node',
id: 'building_1',
type: 'building',
parentId: null,
visible: true,
metadata: {},
children: [],
} as unknown as AnyNode,
]
expect(computeSceneBoundsXZ(barren)).toBeNull()
})
test('computes bounds from wall endpoints', () => {
const nodes: AnyNode[] = [makeWall([0, 0], [4, 0]), makeWall([4, 0], [4, 3])]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
expect(bounds!.min).toEqual([0, 0])
expect(bounds!.max).toEqual([4, 3])
expect(bounds!.size).toEqual([4, 3])
expect(bounds!.center).toEqual([2, 1.5])
})
test('includes zone polygons', () => {
const nodes: AnyNode[] = [
makeZone([
[-10, -5],
[10, -5],
[10, 5],
[-10, 5],
]),
]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
expect(bounds!.min).toEqual([-10, -5])
expect(bounds!.max).toEqual([10, 5])
expect(bounds!.size).toEqual([20, 10])
})
test('ignores the default 30×30 site bootstrap polygon', () => {
const nodes: AnyNode[] = [
makeSite([
[-15, -15],
[15, -15],
[15, 15],
[-15, 15],
]),
makeWall([1, 1], [2, 2]),
]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
// Only the wall should count — the default site polygon is skipped.
expect(bounds!.min).toEqual([1, 1])
expect(bounds!.max).toEqual([2, 2])
})
test('honours a non-default site polygon', () => {
const nodes: AnyNode[] = [
makeSite([
[-25, -20],
[25, -20],
[25, 20],
[-25, 20],
]),
]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
expect(bounds!.min).toEqual([-25, -20])
expect(bounds!.max).toEqual([25, 20])
})
test('combines walls, zones and positions across the flat dict', () => {
const nodes: Record<string, AnyNode> = {
wallA: makeWall([-8, -3], [4, -3]),
wallB: makeWall([4, -3], [4, 6]),
zoneA: makeZone([
[-8, -3],
[4, -3],
[4, 6],
[-8, 6],
]),
item1: {
object: 'node',
id: 'item_1',
type: 'item',
parentId: null,
visible: true,
metadata: {},
position: [7, 0, 8],
rotation: [0, 0, 0],
scale: [1, 1, 1],
children: [],
asset: {
id: 'a',
category: 'furniture',
name: 'Chair',
thumbnail: '',
src: '',
dimensions: [1, 1, 1],
offset: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
} as unknown as AnyNode,
}
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
expect(bounds!.min).toEqual([-8, -3])
expect(bounds!.max).toEqual([7, 8])
})
test('handles a single degenerate point with a minimum extent', () => {
const nodes: AnyNode[] = [makeWall([2, 2], [2, 2])]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
expect(bounds!.size[0]).toBeGreaterThan(0)
expect(bounds!.size[1]).toBeGreaterThan(0)
expect(bounds!.center).toEqual([2, 2])
})
test('skips non-finite coordinates', () => {
const nodes: AnyNode[] = [makeWall([Number.NaN, 0], [4, 2]), makeWall([0, 0], [1, 1])]
const bounds = computeSceneBoundsXZ(nodes)
expect(bounds).not.toBeNull()
// NaN should be ignored; the usable points are (4,2), (0,0), (1,1).
expect(bounds!.min).toEqual([0, 0])
expect(bounds!.max).toEqual([4, 2])
})
})