139 lines
5.4 KiB
TypeScript

import { describe, expect, it } from 'bun:test'
import {
cycleSnappingModeIn,
DEFAULT_SNAPPING_MODE,
defaultSnappingModeFor,
nextSnappingMode,
resolveSnapFlags,
SNAPPING_MODES,
snapContextOf,
snappingModesFor,
} from './snapping-mode'
describe('resolveSnapFlags', () => {
it('default mode is grid', () => {
expect(DEFAULT_SNAPPING_MODE).toBe('grid')
})
it("modes are exclusive: 'grid' snaps to the lattice only", () => {
expect(resolveSnapFlags('grid')).toEqual({ grid: true, magnetic: false, angles: false })
})
it("'off' disables grid, magnetic, and angles", () => {
expect(resolveSnapFlags('off')).toEqual({ grid: false, magnetic: false, angles: false })
})
it("'lines' keeps magnetic but drops the grid lattice and angle lock", () => {
expect(resolveSnapFlags('lines')).toEqual({ grid: false, magnetic: true, angles: false })
})
it("'angles' keeps the angle lock but drops grid and magnetic", () => {
expect(resolveSnapFlags('angles')).toEqual({ grid: false, magnetic: false, angles: true })
})
it("'lines' and 'angles' are distinct", () => {
expect(resolveSnapFlags('lines')).not.toEqual(resolveSnapFlags('angles'))
})
it('cycles through every mode and wraps', () => {
const seen = [DEFAULT_SNAPPING_MODE]
let mode = DEFAULT_SNAPPING_MODE
for (let i = 0; i < SNAPPING_MODES.length - 1; i += 1) {
mode = nextSnappingMode(mode)
seen.push(mode)
}
expect(seen).toEqual(SNAPPING_MODES)
expect(nextSnappingMode(mode)).toBe(DEFAULT_SNAPPING_MODE)
})
})
describe('per-context snapping', () => {
it('items default to grid with no angle lock', () => {
expect(defaultSnappingModeFor('item')).toBe('grid')
expect(snappingModesFor('item')).toEqual(['lines', 'grid', 'off'])
expect(snappingModesFor('item')).not.toContain('angles')
})
it('walls default to grid and expose the angle lock; polygons do NOT', () => {
expect(defaultSnappingModeFor('wall')).toBe('grid')
expect(defaultSnappingModeFor('polygon')).toBe('grid')
expect(snappingModesFor('wall')).toContain('angles')
// Angle lock is wall/fence-only — slabs, curves and translates never get it.
expect(snappingModesFor('polygon')).not.toContain('angles')
expect(snappingModesFor('polygon')).toEqual(['grid', 'lines', 'off'])
})
it('cycles within the context set and clamps a foreign value', () => {
expect(cycleSnappingModeIn('item', 'lines')).toBe('grid')
expect(cycleSnappingModeIn('item', 'off')).toBe('lines')
// 'angles' isn't an item mode → restart at the first entry
expect(cycleSnappingModeIn('item', 'angles')).toBe('lines')
})
})
describe('snapContextOf (profile-driven, node-declared)', () => {
// Stands in for the registry's declared `def.snapProfile` (the only per-kind
// data) — the resolver itself has no kind switch.
const declared: Record<string, 'item' | 'structural'> = {
wall: 'structural',
fence: 'structural',
item: 'item',
slab: 'structural',
ceiling: 'structural',
roof: 'structural',
zone: 'structural',
}
const profileOf = (t: string) => declared[t]
const ctx = (
scope: { kind: string; nodeType?: string; reshape?: string; tool?: string },
mode = 'select',
tool: string | null = null,
) => snapContextOf({ scope, mode, tool, profileOf })
it('translating a whole structural node has no angle (polygon, not wall)', () => {
expect(ctx({ kind: 'moving', nodeType: 'wall' })).toBe('polygon')
expect(ctx({ kind: 'moving', nodeType: 'slab' })).toBe('polygon')
expect(ctx({ kind: 'placing', nodeType: 'item' }, 'build', 'item')).toBe('item')
})
it('endpoint reshape is angle-bearing (wall); curve + polygon vertex edits are not', () => {
expect(ctx({ kind: 'reshaping', reshape: 'endpoint' })).toBe('wall')
expect(ctx({ kind: 'reshaping', reshape: 'curve' })).toBe('polygon')
expect(ctx({ kind: 'reshaping', reshape: 'boundary' })).toBe('polygon')
expect(ctx({ kind: 'reshaping', reshape: 'hole' })).toBe('polygon')
})
it('drafting a structural kind (wall OR slab) is angle-bearing (wall)', () => {
expect(ctx({ kind: 'idle' }, 'build', 'wall')).toBe('wall')
expect(ctx({ kind: 'idle' }, 'build', 'slab')).toBe('wall')
expect(ctx({ kind: 'idle' }, 'build', 'item')).toBe('item')
expect(ctx({ kind: 'idle' }, 'select', null)).toBeNull()
})
it('an undeclared kind (no snapProfile) gets no snap context', () => {
expect(ctx({ kind: 'moving', nodeType: 'door' })).toBeNull()
expect(ctx({ kind: 'idle' }, 'build', 'shelf')).toBeNull()
})
it('drafting a non-directional structural kind is angle-less (polygon, not wall)', () => {
// Roof / stair / elevator are placed as footprints, not directional draws →
// declared `snapDraftDirectional: false`, so their draft context drops the
// angle-lock mode. Directional structural kinds (no flag) stay `wall`.
const draftDirectionalOf = (t: string) => t !== 'roof'
const draftCtx = (tool: string) =>
snapContextOf({ scope: { kind: 'idle' }, mode: 'build', tool, profileOf, draftDirectionalOf })
expect(draftCtx('roof')).toBe('polygon')
expect(draftCtx('wall')).toBe('wall')
// Also via the explicit `drafting` scope path.
expect(
snapContextOf({
scope: { kind: 'drafting', tool: 'roof' },
mode: 'build',
tool: 'roof',
profileOf,
draftDirectionalOf,
}),
).toBe('polygon')
})
})