ai-chatbot/packages/core/src/lib/space-detection.test.ts
Wassim SAMAD f773e6b8c5 feat(editor): placement & interaction overhaul — FSM spine, bug tracks, perf
Implements plans/editor-placement-interaction-overhaul.md: an authoritative
interaction-scope state machine plus the catalogued placement/interaction
fixes, and split-view floor-plan performance.

- Interaction-scope spine (lib/interaction/* + store/use-interaction-scope),
  driven from central useEditor setters; overlay scoping (zone labels,
  context badges, floating action menu) reads resolveOverlayPolicy.
- Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity,
  auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset,
  per-frame jank.
- Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier
  model (Shift=cycle, Alt=free place, Ctrl=grid step).
- Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle
  rig hides during a whole-node move; rotate gizmo advertises Shift=free
  rotation in the HUD and hides the move cross while rotating.
- Floor-plan perf: pause live reactivity while in 3D-only view; per-node
  geometry cache so only changed nodes rebuild on a drag; hoist wall miters
  to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:04:58 -04:00

116 lines
3.3 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { CeilingNode, SlabNode, WallNode } from '../schema'
import { planAutoCeilingsForLevel, planAutoSlabsForLevel } from './space-detection'
const square: Array<[number, number]> = [
[0, 0],
[4, 0],
[4, 3],
[0, 3],
]
function roomPolygon() {
return square.map(([x, y]) => ({ x, y }))
}
function squareWalls(height = 2.5) {
return [
WallNode.parse({ start: [0, 0], end: [4, 0], height }),
WallNode.parse({ start: [4, 0], end: [4, 3], height }),
WallNode.parse({ start: [4, 3], end: [0, 3], height }),
WallNode.parse({ start: [0, 3], end: [0, 0], height }),
]
}
function slab(elevation: number) {
return SlabNode.parse({
polygon: square,
elevation,
autoFromWalls: true,
})
}
describe('planAutoCeilingsForLevel', () => {
test('creates auto ceilings at the top of the room walls', () => {
const created = planAutoCeilingsForLevel([roomPolygon()], [], {
walls: squareWalls(),
slabs: [slab(0.05)],
}).create[0]
expect(created?.height).toBeCloseTo(2.55)
})
test('updates existing auto ceiling height when the slab elevation changes', () => {
const ceiling = CeilingNode.parse({
polygon: square,
height: 2.55,
autoFromWalls: true,
})
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
walls: squareWalls(),
slabs: [slab(0.4)],
})
expect(plan.update).toHaveLength(1)
expect(plan.update[0]?.id).toBe(ceiling.id)
expect(plan.update[0]?.data.polygon).toBeUndefined()
expect(plan.update[0]?.data.height).toBeCloseTo(2.9)
})
test('updates existing auto ceiling height when wall height changes', () => {
const ceiling = CeilingNode.parse({
polygon: square,
height: 2.55,
autoFromWalls: true,
})
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
walls: squareWalls(3),
slabs: [slab(0.05)],
})
expect(plan.update).toHaveLength(1)
expect(plan.update[0]?.data.height).toBeCloseTo(3.05)
})
test('does not replace a manual ceiling with an auto ceiling', () => {
const manualCeiling = CeilingNode.parse({
polygon: square,
height: 2.5,
autoFromWalls: false,
})
const plan = planAutoCeilingsForLevel([roomPolygon()], [manualCeiling], {
walls: squareWalls(),
slabs: [slab(0.4)],
})
expect(plan.create).toHaveLength(0)
expect(plan.update).toHaveLength(0)
})
})
describe('planAutoSlabsForLevel', () => {
test('matches two identical rooms to their own existing auto-slabs without churn', () => {
// Two rooms with identical polygon signatures previously collided in a
// signature-keyed Map, so one detected room never matched an existing slab
// and churned (delete + recreate) on every pass.
const slabA = slab(0.05)
const slabB = slab(0.05)
const plan = planAutoSlabsForLevel([roomPolygon(), roomPolygon()], [slabA, slabB])
expect(plan.create).toHaveLength(0)
expect(plan.delete).toHaveLength(0)
expect(plan.update).toHaveLength(0)
})
test('deletes an extra auto-slab when only one identical room is detected', () => {
const plan = planAutoSlabsForLevel([roomPolygon()], [slab(0.05), slab(0.05)])
expect(plan.create).toHaveLength(0)
expect(plan.delete).toHaveLength(1)
})
})