/* global window, React */ // ============================================================ // Trip detail — Tab 4: 3D load schematic // ============================================================ const { useEffect, useRef } = React; const ORDER_COLORS = [ { fill: '#E8893C', edge: '#A8631F' }, // orange { fill: '#F0B830', edge: '#A8801E' }, // yellow { fill: '#C13333', edge: '#6B1A1A' }, // red (brand) { fill: '#5A3FB8', edge: '#3F2A8A' }, // purple { fill: '#2F7A4A', edge: '#1F5A35' }, // green ]; function drawIso(canvas, orders) { const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const W = canvas.clientWidth; const H = canvas.clientHeight; canvas.width = W * dpr; canvas.height = H * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); // Trailer: 53ft × 8.5ft × 9ft (interior). Pallet 48"×40" = 4×4 ft. // Grid: 13 long × 2 wide × 1 tall = 26 floor positions. We'll do 13×2 floor + double-stack for some. // Simplify: 13 long × 2 wide = 26 floor positions; some orders stack to 32 max. const isoX = (x, y) => (x - y) * 0.866; const isoY = (x, y) => (x + y) * 0.5; // Layout origin const scale = Math.min((W - 60) / (13 * 0.866 * 2 + 5), (H - 80) / (13 * 0.5 + 2 * 0.5 + 4)); const PW = scale * 1.0; // pallet plan width (4ft slot) const PD = scale * 1.0; // pallet plan depth const PH = scale * 1.2; // pallet stack height (~5ft visual) const offX = W / 2 - (13 * 0.866 * PW) / 2 + (2 * 0.866 * PW) / 2; const offY = 40; // Build pallet placement list — each order takes N pallets in sequence // Floor: 13 cols × 2 rows = 26 slots. If total > 26, stack from end. const totalPallets = orders.reduce((a, o) => a + o.pallets, 0); let palettes = []; let orderIdx = 0; let inOrder = 0; for (let i = 0; i < totalPallets; i++) { while (orderIdx < orders.length && inOrder >= orders[orderIdx].pallets) { orderIdx++; inOrder = 0; } if (orderIdx >= orders.length) break; palettes.push({ orderIdx }); inOrder++; } // Place into slots: floor first (col-major: col 0 row 0, col 0 row 1, col 1 row 0...) const placements = []; const FLOOR = 26; for (let i = 0; i < palettes.length; i++) { let col, row, layer; if (i < FLOOR) { col = Math.floor(i / 2); row = i % 2; layer = 0; } else { const j = i - FLOOR; col = Math.floor(j / 2); row = j % 2; layer = 1; } placements.push({ col, row, layer, orderIdx: palettes[i].orderIdx }); } // Draw trailer floor outline (iso) function drawBox(x, y, z, w, d, h, fill, edge) { // x,y,z = grid coords. w,d,h = grid units. const x1 = offX + isoX(x, y) * PW; const y1 = offY + isoY(x, y) * PW - z * PH; const x2 = offX + isoX(x + w, y) * PW; const y2 = offY + isoY(x + w, y) * PW - z * PH; const x3 = offX + isoX(x + w, y + d) * PW; const y3 = offY + isoY(x + w, y + d) * PW - z * PH; const x4 = offX + isoX(x, y + d) * PW; const y4 = offY + isoY(x, y + d) * PW - z * PH; const ht = h * PH; // top ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4); ctx.closePath(); ctx.fillStyle = fill; ctx.fill(); ctx.strokeStyle = edge; ctx.lineWidth = 1; ctx.stroke(); // right face ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(x2, y2 + ht); ctx.lineTo(x3, y3 + ht); ctx.lineTo(x3, y3); ctx.closePath(); ctx.fillStyle = shade(fill, -0.2); ctx.fill(); ctx.strokeStyle = edge; ctx.stroke(); // front face ctx.beginPath(); ctx.moveTo(x3, y3); ctx.lineTo(x3, y3 + ht); ctx.lineTo(x4, y4 + ht); ctx.lineTo(x4, y4); ctx.closePath(); ctx.fillStyle = shade(fill, -0.35); ctx.fill(); ctx.strokeStyle = edge; ctx.stroke(); } function shade(hex, amt) { const c = hex.replace('#', ''); const r = parseInt(c.slice(0, 2), 16); const g = parseInt(c.slice(2, 4), 16); const b = parseInt(c.slice(4, 6), 16); const adj = (v) => Math.max(0, Math.min(255, Math.round(v + 255 * amt))); return `rgb(${adj(r)},${adj(g)},${adj(b)})`; } // Trailer floor (light) drawBox(0, 0, -0.05, 13, 2, 0.05, '#E2DED1', '#B5B2A4'); // Trailer back wall (light vertical face) // Just draw a faint outline ctx.save(); ctx.globalAlpha = 0.4; // back wall vertical (at x=0) const wallX = offX + isoX(0, 0) * PW; const wallTopY = offY + isoY(0, 0) * PW - 2.2 * PH; const wallBotY = offY + isoY(0, 0) * PW; ctx.strokeStyle = '#8E8B7E'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(wallX, wallBotY); ctx.lineTo(wallX, wallTopY); ctx.stroke(); ctx.restore(); // Draw pallets back-to-front (so closer ones cover farther) placements.sort((a, b) => { // draw farthest first: lower col + row first, then lower layer first if (a.col !== b.col) return a.col - b.col; if (a.row !== b.row) return a.row - b.row; return a.layer - b.layer; }); for (const p of placements) { const c = ORDER_COLORS[p.orderIdx % ORDER_COLORS.length]; drawBox(p.col, p.row, p.layer * 1.0, 0.95, 0.95, 0.95, c.fill, c.edge); } // Direction arrow + labels ctx.fillStyle = '#5A584D'; ctx.font = '500 11px JetBrains Mono, monospace'; ctx.textAlign = 'left'; ctx.fillText('NOSE', offX + isoX(0, 1) * PW - 18, offY + isoY(0, 1) * PW + 12); ctx.textAlign = 'right'; ctx.fillText('REAR DOORS →', offX + isoX(13, 1) * PW + 30, offY + isoY(13, 1) * PW + 12); // Capacity meter ctx.fillStyle = '#8E8B7E'; ctx.font = '10px JetBrains Mono, monospace'; ctx.textAlign = 'left'; ctx.fillText(`${placements.length} pallets · ${Math.round((placements.length / 32) * 100)}% positions`, 12, H - 8); } function LoadCanvas({ trip }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; drawIso(ref.current, trip.orders); const handler = () => drawIso(ref.current, trip.orders); window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, [trip]); return ; } function Axle({ a }) { const pct = Math.min(100, Math.round((parseInt(a.value.replace(/,/g, ''), 10) / parseInt(a.limit.replace(/,/g, ''), 10)) * 100)); return (