/** * Smoke test for the Warehouse Builder pure utils (no DOM, no iDempiere). * npx tsx reference/test-scripts/warehouse-layout-smoke.mts */ import assert from 'node:assert/strict' import { createLayout, createObject, createZone, parseLayout } from '../../app/utils/warehouseLayout/schema' import type { WbShelf, WbPallet } from '../../app/utils/warehouseLayout/schema' import { bayCodes, layoutBays, mergeWall, orthoWall, rotatedPatch, shelfBays, worldSide, zoneMembership, findCollision, rectCrossesWall } from '../../app/utils/warehouseLayout/geometry' import { autoLayoutFromLocators, buildLocatorIndex, computeCodeIndex, mapLocatorRow } from '../../app/utils/warehouseLayout/wiring' import { buildAssignments, computePickOrder } from '../../app/utils/warehouseLayout/pick' // --- sides / rotation assert.equal(worldSide('front', 0), 'S'); assert.equal(worldSide('front', 90), 'W') assert.equal(worldSide('front', 180), 'N'); assert.equal(worldSide('front', 270), 'E') assert.equal(worldSide('back', 0), 'N') // --- codes assert.deepEqual(bayCodes({ count: 3, start: '01', step: 1, pad: 2, direction: 'asc' }), ['01', '02', '03']) assert.deepEqual(bayCodes({ count: 3, start: '1', step: 2, pad: 2, direction: 'asc' }), ['01', '03', '05']) assert.deepEqual(bayCodes({ count: 3, start: '01', step: 1, pad: 2, direction: 'desc' }), ['03', '02', '01']) assert.deepEqual(bayCodes({ count: 3, start: 'A', step: 1, pad: 2, direction: 'asc' }), ['A', 'B', 'C']) assert.deepEqual(bayCodes({ count: 2, start: 'A1', step: 1, pad: 2, direction: 'asc' }), ['A1', 'A2']) // --- bays of a 3 m shelf at (2,2), front = S const s = createObject('shelf', 2, 2, { rackCode: 'A', w: 3, d: 1, levels: ['01', '02'] }) as WbShelf let bays = shelfBays(s) assert.equal(bays.length, 3) assert.deepEqual([bays[0].x, bays[0].w, bays[0].cx, bays[0].cy], [2, 1, 2.5, 2.5]) assert.deepEqual(bays[0].access, { x: 2.5, y: 3.5 }) // 0.5 m below the bottom edge assert.equal(bays[0].side, 'S') // rotated 90: vertical, bay 0 at the top, front = W const r = { ...s, ...rotatedPatch(s) } assert.equal(r.rotation, 90); assert.equal(r.w, 1); assert.equal(r.d, 3) bays = shelfBays(r as WbShelf) assert.equal(bays[0].code, '01'); assert.ok(bays[0].cy < bays[2].cy, 'bay 0 on top') assert.equal(bays[0].side, 'W'); assert.equal(bays[0].access.x, r.x - 0.5) // 180: bay 0 at the right, front N const r2 = { ...s, rotation: 180 } as WbShelf bays = shelfBays(r2); assert.ok(bays[0].cx > bays[2].cx); assert.equal(bays[0].side, 'N') // position stays on the 0.5 grid after 4 rotations let q: any = { ...s, w: 2.5, d: 1.1 } for (let i = 0; i < 4; i++) q = { ...q, ...rotatedPatch(q) } assert.ok(Number.isInteger(q.x * 2) && Number.isInteger(q.y * 2)) // --- walls assert.deepEqual(orthoWall(0.2, 0.1, 5.3, 0.4), [0, 0, 5.5, 0]) assert.deepEqual(orthoWall(1, 1, 1.2, 4), [1, 1, 1, 4]) let walls = mergeWall([], [0, 0, 3, 0]); walls = mergeWall(walls, [3, 0, 6, 0]); walls = mergeWall(walls, [0, 2, 0, 5]) assert.deepEqual([...walls].sort(), [[0, 0, 6, 0], [0, 2, 0, 5]]) assert.ok(rectCrossesWall({ x: 1, y: 1, w: 2, d: 2 }, [[2, 0, 2, 10]])) assert.ok(!rectCrossesWall({ x: 1, y: 1, w: 2, d: 2 }, [[3, 0, 3, 10]]), 'wall on the border is fine') // --- collision const layout = createLayout({ grid: { width: 30, height: 20 } }) layout.objects.push(s) assert.equal(findCollision({ x: 4, y: 2, w: 2, d: 1 }, layout.objects), s.key) assert.equal(findCollision({ x: 5, y: 2, w: 2, d: 1 }, layout.objects), null) // --- facing pair + pallet + zones + serpentine const b = createObject('shelf', 2, 6, { rackCode: 'B', w: 3, d: 1, levels: ['01'], accessSide: 'back' }) as WbShelf // faces N → aisle between A (y 3..6) const c = createObject('shelf', 2, 9, { rackCode: 'C', w: 3, d: 1, levels: ['01'] }) as WbShelf // faces S → next aisle const p = createObject('pallet', 8, 4, { locatorId: 99 }) as WbPallet layout.objects.push(b, c, p) layout.objects.push(createObject('door', 0, 1, { doorType: 'in', w: 0.5, d: 2 })) layout.zones.push(createZone(0, 0, 10, 5, 0, { code: 'ZA' }), createZone(0, 5, 10, 10, 1, { code: 'ZB' })) const allBays = layoutBays(layout) const zm = zoneMembership(layout, allBays) assert.equal(zm.get(`${s.key}#0`), 'ZA'); assert.equal(zm.get(`${c.key}#0`), 'ZB'); assert.equal(zm.get(`${p.key}#0`), 'ZA') const { order, aisles } = computePickOrder(layout, allBays) assert.equal(aisles.length, 2, 'A+B share an aisle, C alone: ' + JSON.stringify(aisles)) // A faces S, B faces N, both access points at y≈3.5/5.5 → one aisle; C access at 10.5 → second assert.ok(order.get(`${s.key}#0`)! < order.get(`${c.key}#0`)!) // --- wiring const rows = [ { id: 1, Value: 'A-01-01', X: 'A', Y: '01', Z: '01' }, { id: 2, Value: 'A-01-02', X: 'A', Y: '01', Z: '02' }, { id: 3, Value: 'A-02-01', X: 'A', Y: '02', Z: '01', LayoutObjectKey: 'someone-else' }, { id: 99, Value: 'P-01-01', X: 'P', Y: '01', Z: '01' }, { id: 50, Value: 'Q-01-01', X: 'Q', Y: '01', Z: '01' } ].map(mapLocatorRow) const idx = buildLocatorIndex(rows) const ci = computeCodeIndex(layout, allBays, idx) const byCode = (code: string) => ci.byCode.get(code.toLowerCase())! assert.equal(byCode('A-01-01').status, 'exists-free'); assert.equal(byCode('A-02-01').status, 'exists-mapped-elsewhere') assert.equal(byCode('A-03-01').status, 'missing'); assert.equal(byCode('P-01-01').status, 'exists-free') assert.equal(ci.counts.missing, 9 /* A-02-02, A-03-01, A-03-02, B×3, C×3 */, JSON.stringify(ci.counts)) const { assignments } = buildAssignments(layout, allBays, ci, zm, 0) const a1 = assignments.find(a => a.locatorId === 1)! assert.deepEqual([a1.posX, a1.posY, a1.side, a1.zone], [2.5, 3.5, 'S', 'ZA']) assert.ok(assignments.find(a => a.locatorId === 2)!.pickSeqNo === a1.pickSeqNo + 1, 'level 2 right after level 1') assert.ok(assignments.some(a => a.locatorId === 3), 'mapped-elsewhere is still assigned (steal)') // --- parse round trip const parsed = parseLayout(JSON.stringify(layout)) assert.ok(parsed.layout); assert.equal(parsed.layout!.objects.length, layout.objects.length) assert.equal(parseLayout('{"schemaVersion":2}').layout, null) assert.equal(parseLayout('nope').layout, null) // --- import auto layout const auto = autoLayoutFromLocators(rows) assert.equal(auto.objects.length, 3) // A, P, Q const A = auto.objects.find(o => o.rackCode === 'A')! assert.equal(A.bays.count, 2); assert.deepEqual(A.levels, ['01', '02']) assert.equal(auto.objects[1].accessSide, 'back') console.log('warehouse-layout smoke: OK —', { bays: allBays.length, entries: ci.entries.length, counts: ci.counts, aisles: aisles.map(a => a.bayKeys.length) }) // --- magnetic snapping, door docking, camera coverage (2026-10-31) { const { snapLines, snapRectMove, snapPoint, dockDoorToWall, doorOnWall, cameraCoverage, coverageUnionArea } = await import('../../app/utils/warehouseLayout/geometry') const lay = createLayout(30, 20) lay.walls = [[0, 0, 30, 0], [30, 0, 30, 20], [30, 20, 0, 20], [0, 20, 0, 0], [12.3, 0, 12.3, 8]] const lines = snapLines(lay) assert.ok(lines.some(l => l.axis === 'x' && l.v === 12.3), 'inner wall is a snap line') const mv = snapRectMove(lines, { x: 12.5, y: 2, w: 4, d: 3 }, 0.4) assert.equal(Math.round((12.5 + mv.dx) * 100) / 100, 12.3, 'office snaps onto the 12.3 wall') const pt = snapPoint(lines, 12.45, 3, 0.3) assert.equal(pt.x, 12.3) const dock = dockDoorToWall({ x: 5, y: 0.8, w: 3, d: 0.5 }, lay.walls) assert.ok(dock && dock.horizontal && dock.rect.y === -0.25 && dock.rect.x === 5, 'gate docks centred on the north wall') assert.ok(doorOnWall(dock!.rect, lay.walls)) assert.equal(dockDoorToWall({ x: 15, y: 10, w: 3, d: 0.5 }, lay.walls), null, 'no wall within 2 m → cannot stand free') const vdock = dockDoorToWall({ x: 11.9, y: 3, w: 1, d: 0.3 }, lay.walls) assert.ok(vdock && !vdock.horizontal && vdock.rect.w === 0.3 && vdock.rect.d === 1 && vdock.rect.x === 12.15, 'door turns along the vertical wall') const cam = createObject('camera', 2, 2, { heading: 135, fov: 90, range: 50 }) as any lay.objects.push(cam) const cov = cameraCoverage(cam, lay) assert.ok(cov.points.length > 20 && cov.area > 50 && cov.area < 30 * 20, 'cone clipped by the walls: ' + cov.area) assert.ok(cov.points.every(([x, y]) => x >= -0.01 && x <= 30.01 && y >= -0.01 && y <= 20.01), 'cone stays inside the hall') lay.objects.push(createObject('shelf', 6, 6, { w: 3, d: 1 })) const cov2 = cameraCoverage(cam, lay) assert.ok(cov2.area < cov.area, 'a rack casts a shadow') const union = coverageUnionArea([cov2, cov2], { x: 0, y: 0, w: 30, d: 20 }) assert.ok(Math.abs(union - cov2.area) < cov2.area * 0.1, 'union counts overlaps once: ' + union + ' vs ' + cov2.area) const parsed = parseLayout(JSON.stringify(lay)).layout! assert.equal((parsed.objects.find(o => o.type === 'camera') as any).fov, 90) console.log('snapping / doors / cameras ok') }