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>
116 lines
3.3 KiB
TypeScript
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)
|
|
})
|
|
})
|