/** * Warehouse Builder — pick preparation: serpentine walking order + the Apply payload. * * This pass only PREPARES pick routing: every mapped locator gets its access point * (LayoutPosX/Y + LayoutSide), its zone and a deterministic `PickSeqNo` so any picker can * `orderby PickSeqNo`. The real shortest-path engine (walkable grid from walls/objects) comes * later and only has to rewrite the same columns. * * Serpentine heuristic: racks are grouped into aisles by the coordinate of their access * faces (greedy clustering within `pick.aisleGap`, only faces that look at each other), aisles * are walked in order of position starting at the door, alternating direction per aisle; * levels bottom→top. `PickSeqNo = floorNo·100000 + bayOrdinal·10 + levelIdx`. */ import type { WbDoor, WbLayout, WbShelf, WbSide } from './schema' import { lengthAxis } from './geometry' import type { WbBay } from './geometry' import type { CodeIndex } from './wiring' export interface WbAssignment { locatorId: number posX: number posY: number side: WbSide pickSeqNo: number objectKey: string zone: string | null code: string status: string } interface Face { bay: WbBay; coord: number; along: number; cy: number; faces: 1 | -1; aisleNo?: number } const majorityAxis = (layout: WbLayout): 'x' | 'y' => { let x = 0, y = 0 for (const o of layout.objects) if (o.type === 'shelf') { if (lengthAxis(o.rotation) === 'x') x += o.w; else y += o.d } return y > x ? 'y' : 'x' } /** bayKey → 1-based ordinal in walking order. */ export function computePickOrder (layout: WbLayout, bays: WbBay[]): { order: Map; aisles: Array<{ no: number; coord: number; bayKeys: string[] }> } { const axis = layout.pick.aisleAxis === 'auto' ? majorityAxis(layout) : layout.pick.aisleAxis const gap = layout.pick.aisleGap || 3 const shelves = new Map(layout.objects.filter(o => o.type === 'shelf').map(o => [o.key, o as WbShelf])) // aisle coordinate = access point across the aisle axis; along = position along the aisle const faces: Face[] = bays.filter(b => !b.skip).map(b => { const coord = axis === 'x' ? b.access.y : b.access.x const along = axis === 'x' ? b.access.x : b.access.y const centre = axis === 'x' ? b.cy : b.cx // +1 = the rack lies BEFORE the aisle (faces towards +coord), -1 = after it const facesDir: 1 | -1 = coord >= centre ? 1 : -1 return { bay: b, coord, along, cy: centre, faces: facesDir, aisleNo: shelves.get(b.objectKey)?.pick?.aisleNo } }) faces.sort((a, b) => a.coord - b.coord || a.along - b.along) type Cluster = { faces: Face[]; min: number; max: number; forced?: number } const clusters: Cluster[] = [] const forced = new Map() const compatible = (c: Cluster, f: Face) => { if (f.coord - c.min > gap) return false // two racks looking away from each other must not share an aisle for (const m of c.faces) { if (m.faces === 1 && f.faces === -1 && m.cy > f.cy) return false if (m.faces === -1 && f.faces === 1 && m.cy < f.cy) return false } return true } for (const f of faces) { if (f.aisleNo != null) { let c = forced.get(f.aisleNo) if (!c) { c = { faces: [], min: f.coord, max: f.coord, forced: f.aisleNo }; forced.set(f.aisleNo, c); clusters.push(c) } c.faces.push(f); c.min = Math.min(c.min, f.coord); c.max = Math.max(c.max, f.coord) continue } let target: Cluster | null = null for (let i = clusters.length - 1; i >= 0; i--) { const c = clusters[i] if (c.forced != null) continue if (compatible(c, f)) { target = c; break } if (f.coord - c.max > gap) break } if (!target) { target = { faces: [], min: f.coord, max: f.coord }; clusters.push(target) } target.faces.push(f); target.min = Math.min(target.min, f.coord); target.max = Math.max(target.max, f.coord) } const mean = (c: Cluster) => c.faces.reduce((s, f) => s + f.coord, 0) / (c.faces.length || 1) clusters.sort((a, b) => (a.forced != null && b.forced != null) ? a.forced - b.forced : mean(a) - mean(b)) // start near the inbound door when there is one const door = (layout.pick.startDoorKey && layout.objects.find(o => o.key === layout.pick.startDoorKey)) || layout.objects.find(o => o.type === 'door' && (o as WbDoor).doorType !== 'out') || layout.objects.find(o => o.type === 'door') let reverseAisles = false, startDesc = false if (door && clusters.length) { const dCoord = axis === 'x' ? door.y + door.d / 2 : door.x + door.w / 2 const dAlong = axis === 'x' ? door.x + door.w / 2 : door.y + door.d / 2 const first = mean(clusters[0]), last = mean(clusters[clusters.length - 1]) reverseAisles = Math.abs(dCoord - last) < Math.abs(dCoord - first) const alongs = faces.map(f => f.along) const lo = Math.min(...alongs), hi = Math.max(...alongs) startDesc = Math.abs(dAlong - hi) < Math.abs(dAlong - lo) } if (reverseAisles) clusters.reverse() const order = new Map() const aisles: Array<{ no: number; coord: number; bayKeys: string[] }> = [] let ordinal = 0 clusters.forEach((c, k) => { const desc = (k % 2 === 1) !== startDesc const sorted = [...c.faces].sort((a, b) => (desc ? b.along - a.along : a.along - b.along) || a.faces - b.faces) const keys: string[] = [] for (const f of sorted) { ordinal++; order.set(f.bay.key, ordinal); keys.push(f.bay.key) } aisles.push({ no: k + 1, coord: Math.round(mean(c) * 100) / 100, bayKeys: keys }) }) return { order, aisles } } export const pickSeqNo = (floorNo: number, ordinal: number, levelIdx: number): number => (Math.max(0, floorNo) * 100000) + Math.min(ordinal, 9999) * 10 + Math.min(levelIdx, 9) /** The Apply payload: one row per resolved locator (conflicts excluded). */ export function buildAssignments ( layout: WbLayout, bays: WbBay[], codeIndex: CodeIndex, zones: Map, floorNo: number ): { assignments: WbAssignment[]; order: Map } { const { order } = computePickOrder(layout, bays) const bayByKey = new Map(bays.map(b => [b.key, b])) const assignments: WbAssignment[] = [] for (const e of codeIndex.entries) { if (!e.locatorId || e.status === 'conflict' || e.status === 'missing') continue const b = bayByKey.get(e.bayKey) if (!b) continue assignments.push({ locatorId: e.locatorId, posX: b.access.x, posY: b.access.y, side: b.side, pickSeqNo: pickSeqNo(floorNo, order.get(b.key) || 0, e.levelIdx), objectKey: e.objectKey, zone: zones.get(b.key) ?? null, code: e.code, status: e.status }) } return { assignments, order } }