| | |
| | |
| | |
| | |
| |
|
| | const ISO = { |
| | |
| | ANGLE: Math.PI / 6, |
| |
|
| | |
| | TILE_WIDTH: 48, |
| | TILE_HEIGHT: 24, |
| |
|
| | |
| | SHELF_WIDTH: 3, |
| | SHELF_DEPTH: 1, |
| | SHELF_HEIGHT: 2, |
| |
|
| | |
| | COLS: 5, |
| | ROWS: 3, |
| |
|
| | |
| | AISLE_WIDTH: 2, |
| |
|
| | |
| | COLORS: { |
| | floor: '#e2e8f0', |
| | floorGrid: '#cbd5e1', |
| | shelfTop: '#ffffff', |
| | shelfFront: '#f1f5f9', |
| | shelfSide: '#e2e8f0', |
| | shelfBorder: '#94a3b8', |
| | shadow: 'rgba(0, 0, 0, 0.1)', |
| | trolley: [ |
| | '#ef4444', |
| | '#3b82f6', |
| | '#10b981', |
| | '#f59e0b', |
| | '#8b5cf6', |
| | '#06b6d4', |
| | '#ec4899', |
| | '#84cc16', |
| | ], |
| | path: 'rgba(59, 130, 246, 0.3)', |
| | pathActive: 'rgba(59, 130, 246, 0.6)', |
| | }, |
| |
|
| | |
| | animationId: null, |
| | isSolving: false, |
| | trolleyAnimations: new Map(), |
| | currentSolution: null, |
| |
|
| | |
| | canvas: null, |
| | ctx: null, |
| | dpr: 1, |
| | width: 0, |
| | height: 0, |
| | originX: 0, |
| | originY: 0, |
| | |
| | gridOffsetX: 0, |
| | gridOffsetY: 0, |
| | }; |
| |
|
| | |
| | const COLUMNS = ['A', 'B', 'C', 'D', 'E']; |
| | const ROWS = ['1', '2', '3']; |
| |
|
| | |
| | |
| | |
| | function isoToScreen(x, y, z = 0) { |
| | |
| | const centeredX = x - ISO.gridOffsetX; |
| | const centeredY = y - ISO.gridOffsetY; |
| | const screenX = ISO.originX + (centeredX - centeredY) * (ISO.TILE_WIDTH / 2); |
| | const screenY = ISO.originY + (centeredX + centeredY) * (ISO.TILE_HEIGHT / 2) - z * ISO.TILE_HEIGHT; |
| | return { x: screenX, y: screenY }; |
| | } |
| |
|
| | |
| | |
| | |
| | function getTrolleyColor(trolleyId) { |
| | const index = (parseInt(trolleyId) - 1) % ISO.COLORS.trolley.length; |
| | return ISO.COLORS.trolley[index]; |
| | } |
| |
|
| | |
| | |
| | |
| | function initWarehouseCanvas() { |
| | const container = document.getElementById('warehouseContainer'); |
| | if (!container) return; |
| |
|
| | let canvas = document.getElementById('warehouseCanvas'); |
| | if (!canvas) { |
| | canvas = document.createElement('canvas'); |
| | canvas.id = 'warehouseCanvas'; |
| | container.appendChild(canvas); |
| | } |
| |
|
| | ISO.canvas = canvas; |
| | ISO.ctx = canvas.getContext('2d'); |
| | ISO.dpr = window.devicePixelRatio || 1; |
| |
|
| | |
| | const totalWidth = (ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH) * ISO.TILE_WIDTH; |
| | const totalHeight = (ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH) * ISO.TILE_WIDTH; |
| |
|
| | |
| | ISO.width = totalWidth + 200; |
| | ISO.height = totalHeight / 2 + 300; |
| |
|
| | |
| | canvas.width = ISO.width * ISO.dpr; |
| | canvas.height = ISO.height * ISO.dpr; |
| | canvas.style.width = ISO.width + 'px'; |
| | canvas.style.height = ISO.height + 'px'; |
| |
|
| | ISO.ctx.scale(ISO.dpr, ISO.dpr); |
| |
|
| | |
| | const gridSize = ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; |
| | const gridDepth = ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; |
| | ISO.gridOffsetX = gridSize / 2; |
| | ISO.gridOffsetY = gridDepth / 2; |
| |
|
| | |
| | ISO.originX = ISO.width / 2; |
| | ISO.originY = ISO.height / 2 - 50; |
| | } |
| |
|
| | |
| | |
| | |
| | function drawFloor() { |
| | const ctx = ISO.ctx; |
| | const gridSize = ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; |
| | const gridDepth = ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; |
| |
|
| | |
| | for (let x = 0; x < gridSize; x++) { |
| | for (let y = 0; y < gridDepth; y++) { |
| | const p1 = isoToScreen(x, y); |
| | const p2 = isoToScreen(x + 1, y); |
| | const p3 = isoToScreen(x + 1, y + 1); |
| | const p4 = isoToScreen(x, y + 1); |
| |
|
| | ctx.beginPath(); |
| | ctx.moveTo(p1.x, p1.y); |
| | ctx.lineTo(p2.x, p2.y); |
| | ctx.lineTo(p3.x, p3.y); |
| | ctx.lineTo(p4.x, p4.y); |
| | ctx.closePath(); |
| |
|
| | ctx.fillStyle = ISO.COLORS.floor; |
| | ctx.fill(); |
| | ctx.strokeStyle = ISO.COLORS.floorGrid; |
| | ctx.lineWidth = 0.5; |
| | ctx.stroke(); |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function drawShelf(col, row, label) { |
| | const ctx = ISO.ctx; |
| |
|
| | |
| | const gridX = ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH); |
| | const gridY = ISO.AISLE_WIDTH + row * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH); |
| |
|
| | const w = ISO.SHELF_WIDTH; |
| | const d = ISO.SHELF_DEPTH; |
| | const h = ISO.SHELF_HEIGHT; |
| |
|
| | |
| | const topFront = [ |
| | isoToScreen(gridX, gridY + d, h), |
| | isoToScreen(gridX + w, gridY + d, h), |
| | isoToScreen(gridX + w, gridY, h), |
| | isoToScreen(gridX, gridY, h), |
| | ]; |
| |
|
| | const bottomFront = [ |
| | isoToScreen(gridX, gridY + d, 0), |
| | isoToScreen(gridX + w, gridY + d, 0), |
| | ]; |
| |
|
| | const bottomSide = [ |
| | isoToScreen(gridX + w, gridY, 0), |
| | ]; |
| |
|
| | |
| | ctx.beginPath(); |
| | const shadowOffset = 0.3; |
| | const s1 = isoToScreen(gridX + shadowOffset, gridY + d + shadowOffset, 0); |
| | const s2 = isoToScreen(gridX + w + shadowOffset, gridY + d + shadowOffset, 0); |
| | const s3 = isoToScreen(gridX + w + shadowOffset, gridY + shadowOffset, 0); |
| | const s4 = isoToScreen(gridX + shadowOffset, gridY + shadowOffset, 0); |
| | ctx.moveTo(s1.x, s1.y); |
| | ctx.lineTo(s2.x, s2.y); |
| | ctx.lineTo(s3.x, s3.y); |
| | ctx.lineTo(s4.x, s4.y); |
| | ctx.closePath(); |
| | ctx.fillStyle = ISO.COLORS.shadow; |
| | ctx.fill(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(topFront[0].x, topFront[0].y); |
| | ctx.lineTo(topFront[1].x, topFront[1].y); |
| | ctx.lineTo(bottomFront[1].x, bottomFront[1].y); |
| | ctx.lineTo(bottomFront[0].x, bottomFront[0].y); |
| | ctx.closePath(); |
| | ctx.fillStyle = ISO.COLORS.shelfFront; |
| | ctx.fill(); |
| | ctx.strokeStyle = ISO.COLORS.shelfBorder; |
| | ctx.lineWidth = 1; |
| | ctx.stroke(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(topFront[1].x, topFront[1].y); |
| | ctx.lineTo(topFront[2].x, topFront[2].y); |
| | ctx.lineTo(bottomSide[0].x, bottomSide[0].y); |
| | ctx.lineTo(bottomFront[1].x, bottomFront[1].y); |
| | ctx.closePath(); |
| | ctx.fillStyle = ISO.COLORS.shelfSide; |
| | ctx.fill(); |
| | ctx.strokeStyle = ISO.COLORS.shelfBorder; |
| | ctx.stroke(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(topFront[0].x, topFront[0].y); |
| | ctx.lineTo(topFront[1].x, topFront[1].y); |
| | ctx.lineTo(topFront[2].x, topFront[2].y); |
| | ctx.lineTo(topFront[3].x, topFront[3].y); |
| | ctx.closePath(); |
| | ctx.fillStyle = ISO.COLORS.shelfTop; |
| | ctx.fill(); |
| | ctx.strokeStyle = ISO.COLORS.shelfBorder; |
| | ctx.stroke(); |
| |
|
| | |
| | const centerX = (topFront[0].x + topFront[1].x + topFront[2].x + topFront[3].x) / 4; |
| | const centerY = (topFront[0].y + topFront[1].y + topFront[2].y + topFront[3].y) / 4; |
| |
|
| | ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, sans-serif'; |
| | ctx.textAlign = 'center'; |
| | ctx.textBaseline = 'middle'; |
| | ctx.fillStyle = '#475569'; |
| | ctx.fillText(label, centerX, centerY); |
| | } |
| |
|
| | |
| | |
| | |
| | function drawShelves() { |
| | for (let col = 0; col < ISO.COLS; col++) { |
| | for (let row = 0; row < ISO.ROWS; row++) { |
| | const label = COLUMNS[col] + ',' + ROWS[row]; |
| | drawShelf(col, row, label); |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function drawTrolley(x, y, color, trolleyId, progress = 1) { |
| | const ctx = ISO.ctx; |
| | const pos = isoToScreen(x, y, 0.3); |
| |
|
| | |
| | const bodyWidth = 20; |
| | const bodyHeight = 14; |
| | const bodyDepth = 8; |
| |
|
| | |
| | ctx.save(); |
| | ctx.translate(pos.x, pos.y); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.ellipse(0, 6, 12, 6, 0, 0, Math.PI * 2); |
| | ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; |
| | ctx.fill(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(-bodyWidth/2, -bodyDepth); |
| | ctx.lineTo(bodyWidth/2, -bodyDepth); |
| | ctx.lineTo(bodyWidth/2, bodyHeight - bodyDepth); |
| | ctx.lineTo(-bodyWidth/2, bodyHeight - bodyDepth); |
| | ctx.closePath(); |
| | ctx.fillStyle = color; |
| | ctx.fill(); |
| | ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; |
| | ctx.lineWidth = 1; |
| | ctx.stroke(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(-bodyWidth/2, -bodyDepth); |
| | ctx.lineTo(0, -bodyDepth - 6); |
| | ctx.lineTo(bodyWidth/2, -bodyDepth); |
| | ctx.lineTo(0, -bodyDepth + 3); |
| | ctx.closePath(); |
| | const lighterColor = lightenColor(color, 20); |
| | ctx.fillStyle = lighterColor; |
| | ctx.fill(); |
| | ctx.stroke(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.moveTo(-bodyWidth/2 + 3, -bodyDepth - 2); |
| | ctx.lineTo(-bodyWidth/2 + 3, -bodyDepth - 12); |
| | ctx.lineTo(bodyWidth/2 - 3, -bodyDepth - 12); |
| | ctx.lineTo(bodyWidth/2 - 3, -bodyDepth - 2); |
| | ctx.strokeStyle = darkenColor(color, 20); |
| | ctx.lineWidth = 2; |
| | ctx.stroke(); |
| |
|
| | |
| | ctx.beginPath(); |
| | ctx.arc(0, -bodyDepth - 16, 10, 0, Math.PI * 2); |
| | ctx.fillStyle = 'white'; |
| | ctx.fill(); |
| | ctx.strokeStyle = color; |
| | ctx.lineWidth = 2; |
| | ctx.stroke(); |
| |
|
| | ctx.font = 'bold 11px -apple-system, sans-serif'; |
| | ctx.textAlign = 'center'; |
| | ctx.textBaseline = 'middle'; |
| | ctx.fillStyle = color; |
| | ctx.fillText(trolleyId, 0, -bodyDepth - 16); |
| |
|
| | ctx.restore(); |
| | } |
| |
|
| | |
| | |
| | |
| | function lightenColor(hex, percent) { |
| | const num = parseInt(hex.slice(1), 16); |
| | const amt = Math.round(2.55 * percent); |
| | const R = Math.min(255, (num >> 16) + amt); |
| | const G = Math.min(255, ((num >> 8) & 0x00FF) + amt); |
| | const B = Math.min(255, (num & 0x0000FF) + amt); |
| | return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); |
| | } |
| |
|
| | |
| | |
| | |
| | function darkenColor(hex, percent) { |
| | const num = parseInt(hex.slice(1), 16); |
| | const amt = Math.round(2.55 * percent); |
| | const R = Math.max(0, (num >> 16) - amt); |
| | const G = Math.max(0, ((num >> 8) & 0x00FF) - amt); |
| | const B = Math.max(0, (num & 0x0000FF) - amt); |
| | return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function locationToGrid(location) { |
| | if (!location) return { x: 1, y: 1 }; |
| |
|
| | |
| | const shelvingId = location.shelvingId || ''; |
| | const match = shelvingId.match(/\(([A-E]),\s*(\d)\)/); |
| |
|
| | let col = 0, row = 0; |
| | if (match) { |
| | col = COLUMNS.indexOf(match[1]); |
| | row = parseInt(match[2]) - 1; |
| | } |
| |
|
| | |
| | const shelfGridX = ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH); |
| | const shelfGridY = ISO.AISLE_WIDTH + row * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH); |
| |
|
| | |
| | const aisleY = shelfGridY + ISO.SHELF_DEPTH + 0.5; |
| |
|
| | |
| | const side = location.side; |
| | let gridX; |
| | if (side === 'LEFT') { |
| | gridX = shelfGridX - 0.5; |
| | } else { |
| | gridX = shelfGridX + ISO.SHELF_WIDTH + 0.5; |
| | } |
| |
|
| | return { x: gridX, y: aisleY }; |
| | } |
| |
|
| | |
| | |
| | |
| | function getMainAisleY() { |
| | |
| | return (ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH)) + ISO.AISLE_WIDTH + 0.5; |
| | } |
| |
|
| | |
| | |
| | |
| | function getVerticalAisleX(col) { |
| | |
| | return ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) - 0.5; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function buildAislePath(start, end) { |
| | const path = [start]; |
| |
|
| | |
| | const dx = Math.abs(start.x - end.x); |
| | const dy = Math.abs(start.y - end.y); |
| | if (dx < 2 && dy < 2) { |
| | path.push(end); |
| | return path; |
| | } |
| |
|
| | const mainAisleY = getMainAisleY(); |
| |
|
| | |
| | |
| | const startAisleY = Math.max(start.y, mainAisleY - 1); |
| | const endAisleY = Math.max(end.y, mainAisleY - 1); |
| |
|
| | |
| | if (Math.abs(start.y - startAisleY) > 0.5) { |
| | path.push({ x: start.x, y: startAisleY }); |
| | } |
| |
|
| | |
| | if (Math.abs(start.x - end.x) > 0.5) { |
| | path.push({ x: end.x, y: startAisleY }); |
| | } |
| |
|
| | |
| | if (Math.abs(path[path.length - 1].y - end.y) > 0.5) { |
| | path.push({ x: end.x, y: end.y }); |
| | } |
| |
|
| | |
| | const last = path[path.length - 1]; |
| | if (Math.abs(last.x - end.x) > 0.1 || Math.abs(last.y - end.y) > 0.1) { |
| | path.push(end); |
| | } |
| |
|
| | return path; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function buildTrolleyPath(trolley, steps) { |
| | const waypoints = []; |
| | const waypointTypes = []; |
| |
|
| | |
| | if (trolley.location) { |
| | waypoints.push(locationToGrid(trolley.location)); |
| | waypointTypes.push('start'); |
| | } |
| |
|
| | |
| | for (const step of steps) { |
| | if (step.orderItem && step.orderItem.product && step.orderItem.product.location) { |
| | waypoints.push(locationToGrid(step.orderItem.product.location)); |
| | waypointTypes.push('pickup'); |
| | } |
| | } |
| |
|
| | |
| | if (waypoints.length > 1 && trolley.location) { |
| | waypoints.push(locationToGrid(trolley.location)); |
| | waypointTypes.push('end'); |
| | } |
| |
|
| | if (waypoints.length <= 1) { |
| | return { path: waypoints, pickupIndices: [] }; |
| | } |
| |
|
| | |
| | const fullPath = [waypoints[0]]; |
| | const pickupIndices = []; |
| |
|
| | for (let i = 1; i < waypoints.length; i++) { |
| | const segmentPath = buildAislePath(waypoints[i - 1], waypoints[i]); |
| | |
| | for (let j = 1; j < segmentPath.length; j++) { |
| | fullPath.push(segmentPath[j]); |
| | } |
| | |
| | if (waypointTypes[i] === 'pickup') { |
| | pickupIndices.push(fullPath.length - 1); |
| | } |
| | } |
| |
|
| | return { path: fullPath, pickupIndices: pickupIndices }; |
| | } |
| |
|
| | |
| | |
| | |
| | function drawPath(path, color, pickupIndices, active = false) { |
| | if (path.length < 2) return; |
| |
|
| | const ctx = ISO.ctx; |
| | ctx.beginPath(); |
| |
|
| | const start = isoToScreen(path[0].x, path[0].y, 0.1); |
| | ctx.moveTo(start.x, start.y); |
| |
|
| | for (let i = 1; i < path.length; i++) { |
| | const point = isoToScreen(path[i].x, path[i].y, 0.1); |
| | ctx.lineTo(point.x, point.y); |
| | } |
| |
|
| | ctx.strokeStyle = active ? ISO.COLORS.pathActive : ISO.COLORS.path; |
| | ctx.lineWidth = active ? 4 : 2; |
| | ctx.lineCap = 'round'; |
| | ctx.lineJoin = 'round'; |
| | ctx.stroke(); |
| |
|
| | |
| | let pickupNum = 1; |
| | for (const idx of pickupIndices) { |
| | if (idx >= 0 && idx < path.length) { |
| | const point = isoToScreen(path[idx].x, path[idx].y, 0.5); |
| |
|
| | ctx.beginPath(); |
| | ctx.arc(point.x, point.y, 8, 0, Math.PI * 2); |
| | ctx.fillStyle = color; |
| | ctx.fill(); |
| | ctx.strokeStyle = 'white'; |
| | ctx.lineWidth = 2; |
| | ctx.stroke(); |
| |
|
| | ctx.font = 'bold 9px -apple-system, sans-serif'; |
| | ctx.textAlign = 'center'; |
| | ctx.textBaseline = 'middle'; |
| | ctx.fillStyle = 'white'; |
| | ctx.fillText(pickupNum.toString(), point.x, point.y); |
| | pickupNum++; |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function getPositionOnPath(path, progress) { |
| | if (!path || path.length === 0) return { x: 0, y: 0 }; |
| | if (path.length === 1) return path[0]; |
| |
|
| | const totalSegments = path.length - 1; |
| | const segmentProgress = progress * totalSegments; |
| | const currentSegment = Math.min(Math.floor(segmentProgress), totalSegments - 1); |
| | const segmentT = segmentProgress - currentSegment; |
| |
|
| | const start = path[currentSegment]; |
| | const end = path[currentSegment + 1]; |
| |
|
| | |
| | if (!start || !end) return path[0] || { x: 0, y: 0 }; |
| |
|
| | return { |
| | x: start.x + (end.x - start.x) * segmentT, |
| | y: start.y + (end.y - start.y) * segmentT, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | function renderWarehouse(solution) { |
| | if (!ISO.ctx) return; |
| |
|
| | const ctx = ISO.ctx; |
| | ctx.clearRect(0, 0, ISO.width, ISO.height); |
| |
|
| | |
| | drawFloor(); |
| |
|
| | |
| | drawShelves(); |
| |
|
| | if (!solution || !solution.trolleys) return; |
| |
|
| | |
| | const stepLookup = new Map(); |
| | for (const step of solution.trolleySteps || []) { |
| | stepLookup.set(step.id, step); |
| | } |
| |
|
| | |
| | const trolleys = solution.trolleys || []; |
| |
|
| | for (const trolley of trolleys) { |
| | const steps = (trolley.steps || []).map(ref => |
| | typeof ref === 'string' ? stepLookup.get(ref) : ref |
| | ).filter(s => s); |
| |
|
| | const color = getTrolleyColor(trolley.id); |
| | const pathData = buildTrolleyPath(trolley, steps); |
| | const path = pathData.path; |
| | const pickupIndices = pathData.pickupIndices; |
| |
|
| | |
| | if (path.length > 1) { |
| | drawPath(path, color, pickupIndices, ISO.isSolving); |
| | } |
| |
|
| | |
| | const anim = ISO.trolleyAnimations.get(trolley.id); |
| | let pos; |
| |
|
| | if (anim && ISO.isSolving && path.length > 1) { |
| | const now = Date.now(); |
| | const elapsed = now - anim.startTime; |
| | const progress = (elapsed % anim.duration) / anim.duration; |
| | pos = getPositionOnPath(path, progress); |
| | } else if (path.length > 0) { |
| | pos = path[0]; |
| | } else { |
| | pos = locationToGrid(trolley.location); |
| | } |
| |
|
| | if (pos) { |
| | drawTrolley(pos.x, pos.y, color, trolley.id); |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function animate() { |
| | if (!ISO.isSolving) { |
| | ISO.animationId = null; |
| | return; |
| | } |
| |
|
| | |
| | |
| | if (ISO.currentSolution && ISO.currentSolution.trolleys) { |
| | renderWarehouse(ISO.currentSolution); |
| | } |
| |
|
| | ISO.animationId = requestAnimationFrame(animate); |
| | } |
| |
|
| | |
| | |
| | |
| | function startWarehouseAnimation(solution) { |
| | ISO.isSolving = true; |
| | ISO.currentSolution = solution; |
| | ISO.trolleyAnimations.clear(); |
| |
|
| | |
| | const stepLookup = new Map(); |
| | for (const step of solution.trolleySteps || []) { |
| | stepLookup.set(step.id, step); |
| | } |
| |
|
| | for (const trolley of solution.trolleys || []) { |
| | |
| | const stepIds = (trolley.steps || []).map(ref => |
| | typeof ref === 'string' ? ref : ref.id |
| | ); |
| |
|
| | const steps = stepIds.map(id => stepLookup.get(id)).filter(s => s); |
| |
|
| | const pathData = buildTrolleyPath(trolley, steps); |
| | const path = pathData.path; |
| | const duration = Math.max(3000, path.length * 400); |
| |
|
| | ISO.trolleyAnimations.set(trolley.id, { |
| | startTime: Date.now() + parseInt(trolley.id) * 200, |
| | duration: duration, |
| | path: path, |
| | stepSignature: stepIds.join(','), |
| | }); |
| | } |
| |
|
| | if (!ISO.animationId) { |
| | animate(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function updateWarehouseAnimation(solution) { |
| | console.log('[updateWarehouseAnimation] Called with', solution?.trolleys?.length, 'trolleys'); |
| | ISO.currentSolution = solution; |
| |
|
| | |
| | const stepLookup = new Map(); |
| | for (const step of solution.trolleySteps || []) { |
| | stepLookup.set(step.id, step); |
| | } |
| |
|
| | let anyPathChanged = false; |
| |
|
| | for (const trolley of solution.trolleys || []) { |
| | |
| | const stepIds = (trolley.steps || []).map(ref => |
| | typeof ref === 'string' ? ref : ref.id |
| | ); |
| |
|
| | |
| | const steps = stepIds.map(id => stepLookup.get(id)).filter(s => s); |
| |
|
| | const pathData = buildTrolleyPath(trolley, steps); |
| | const path = pathData.path; |
| | const existingAnim = ISO.trolleyAnimations.get(trolley.id); |
| |
|
| | if (existingAnim) { |
| | |
| | const oldSignature = existingAnim.stepSignature || ''; |
| | const newSignature = stepIds.join(','); |
| | const pathChanged = oldSignature !== newSignature; |
| |
|
| | if (pathChanged) { |
| | anyPathChanged = true; |
| | console.log(`[PATH CHANGE] Trolley ${trolley.id}:`, { |
| | old: oldSignature.substring(0, 50), |
| | new: newSignature.substring(0, 50), |
| | oldLen: oldSignature.split(',').filter(x=>x).length, |
| | newLen: stepIds.length |
| | }); |
| | } |
| |
|
| | existingAnim.path = path; |
| | existingAnim.stepSignature = newSignature; |
| | existingAnim.duration = Math.max(3000, path.length * 400); |
| |
|
| | |
| | if (pathChanged) { |
| | existingAnim.startTime = Date.now(); |
| | } |
| | } else { |
| | ISO.trolleyAnimations.set(trolley.id, { |
| | startTime: Date.now(), |
| | duration: Math.max(3000, path.length * 400), |
| | path: path, |
| | stepSignature: stepIds.join(','), |
| | }); |
| | anyPathChanged = true; |
| | } |
| | } |
| |
|
| | |
| | if (anyPathChanged) { |
| | console.log('[RENDER] Paths changed, forcing immediate render'); |
| | |
| | if (ISO.canvas) { |
| | ISO.canvas.style.outline = '3px solid #10b981'; |
| | setTimeout(() => { ISO.canvas.style.outline = 'none'; }, 200); |
| | } |
| | } |
| |
|
| | |
| | renderWarehouse(solution); |
| |
|
| | |
| | if (ISO.isSolving && !ISO.animationId) { |
| | animate(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function stopWarehouseAnimation() { |
| | ISO.isSolving = false; |
| | if (ISO.animationId) { |
| | cancelAnimationFrame(ISO.animationId); |
| | ISO.animationId = null; |
| | } |
| | |
| | if (ISO.currentSolution) { |
| | renderWarehouse(ISO.currentSolution); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function updateLegend(solution, distances) { |
| | const container = document.getElementById('trolleyLegend'); |
| | if (!container) return; |
| |
|
| | container.innerHTML = ''; |
| |
|
| | const stepLookup = new Map(); |
| | for (const step of solution.trolleySteps || []) { |
| | stepLookup.set(step.id, step); |
| | } |
| |
|
| | for (const trolley of solution.trolleys || []) { |
| | const steps = (trolley.steps || []).map(ref => |
| | typeof ref === 'string' ? stepLookup.get(ref) : ref |
| | ).filter(s => s); |
| |
|
| | const color = getTrolleyColor(trolley.id); |
| | const distance = distances ? distances.get(trolley.id) || 0 : 0; |
| |
|
| | const item = document.createElement('div'); |
| | item.className = 'legend-item'; |
| | item.innerHTML = ` |
| | <div class="legend-color" style="background: ${color}"></div> |
| | <span class="legend-text">Trolley ${trolley.id}</span> |
| | <span class="legend-distance">${steps.length} items</span> |
| | `; |
| | container.appendChild(item); |
| | } |
| | } |
| |
|
| | |
| | window.initWarehouseCanvas = initWarehouseCanvas; |
| | window.renderWarehouse = renderWarehouse; |
| | window.startWarehouseAnimation = startWarehouseAnimation; |
| | window.updateWarehouseAnimation = updateWarehouseAnimation; |
| | window.stopWarehouseAnimation = stopWarehouseAnimation; |
| | window.updateLegend = updateLegend; |
| | window.getTrolleyColor = getTrolleyColor; |
| |
|