import { useEffect, useRef, useState } from 'react'; import type { LayoutState, Point } from '../types'; interface RoomCanvasProps { state: LayoutState; onStateChange: (state: LayoutState) => void; mode: 'layout' | 'optimize'; selectedId: string | null; onSelect: (id: string | null) => void; } interface DragState { id: string; offsetX: number; offsetY: number; resizing?: boolean; } export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: RoomCanvasProps) { const canvasRef = useRef(null); const [drag, setDrag] = useState(null); const [zoom] = useState(1); useEffect(() => { draw(); }, [state, selectedId, mode, zoom]); const draw = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.scale(zoom, zoom); ctx.fillStyle = '#12151b'; ctx.fillRect(0, 0, state.room.width, state.room.height); // subtle grid ctx.strokeStyle = 'rgba(255,255,255,0.035)'; ctx.lineWidth = 1; const step = 25; for (let x = 0; x <= state.room.width; x += step) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, state.room.height); ctx.stroke(); } for (let y = 0; y <= state.room.height; y += step) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(state.room.width, y); ctx.stroke(); } ctx.strokeStyle = '#2c3342'; ctx.lineWidth = 2; ctx.strokeRect(0, 0, state.room.width, state.room.height); if (state.entrance) { const e = state.entrance; const w = e.width ?? 20; const h = e.height ?? 20; const selected = selectedId === '__entrance__'; ctx.fillStyle = '#3fbf7f'; ctx.fillRect(e.x, e.y, w, h); ctx.strokeStyle = selected ? '#4f8cff' : 'rgba(0, 0, 0, 0.4)'; ctx.lineWidth = selected ? 2.5 : 1; ctx.strokeRect(e.x, e.y, w, h); ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; ctx.font = '600 10px Inter, sans-serif'; ctx.textBaseline = 'middle'; if (w > 50) ctx.fillText('ENTRANCE', e.x + 6, e.y + h / 2); // resize handle (SE) ctx.fillStyle = selected ? '#4f8cff' : 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(e.x + w - 4, e.y + h - 4, 8, 8); } for (const f of state.furniture) { const b = { x: f.shape.x, y: f.shape.y, w: f.shape.width, h: f.shape.height }; const r = Math.min(8, b.w / 4, b.h / 4); const selected = selectedId === f.id; ctx.globalAlpha = f.lockedForOptimization && mode === 'optimize' ? 0.55 : 1; ctx.beginPath(); roundRect(ctx, b.x, b.y, b.w, b.h, r); ctx.fillStyle = f.color || '#9aa0a6'; ctx.fill(); if (selected) { ctx.shadowColor = '#4f8cff'; ctx.shadowBlur = 14; ctx.strokeStyle = '#4f8cff'; ctx.lineWidth = 2.5; ctx.stroke(); ctx.shadowBlur = 0; } else { ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)'; ctx.lineWidth = 1; ctx.stroke(); } ctx.fillStyle = 'rgba(0, 0, 0, 0.72)'; ctx.font = '600 11px Inter, sans-serif'; ctx.textBaseline = 'middle'; const label = b.w > 44 ? f.name.substring(0, 14) : ''; if (label) ctx.fillText(label, b.x + 7, b.y + b.h / 2); if (f.lockedForOptimization) { ctx.font = '10px Inter, sans-serif'; ctx.fillStyle = 'rgba(0, 0, 0, 0.55)'; ctx.fillText('🔒', b.x + b.w - 15, b.y + 9); } ctx.globalAlpha = 1; } ctx.restore(); }; const getMousePos = (e: React.MouseEvent): Point => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: ((e.clientX - rect.left) * scaleX) / zoom, y: ((e.clientY - rect.top) * scaleY) / zoom }; }; const handleMouseDown = (e: React.MouseEvent) => { const pos = getMousePos(e); if (state.entrance) { const en = state.entrance; const w = en.width ?? 20; const h = en.height ?? 20; const handleX = en.x + w - 4; const handleY = en.y + h - 4; if (Math.abs(pos.x - handleX) <= 6 && Math.abs(pos.y - handleY) <= 6) { onSelect('__entrance__'); setDrag({ id: '__entrance__', offsetX: 0, offsetY: 0, resizing: true }); return; } if (pointInRect(pos, { x: en.x, y: en.y, w, h })) { onSelect('__entrance__'); setDrag({ id: '__entrance__', offsetX: pos.x - en.x, offsetY: pos.y - en.y }); return; } } for (let i = state.furniture.length - 1; i >= 0; i--) { const f = state.furniture[i]; if (pointInRect(pos, { x: f.shape.x, y: f.shape.y, w: f.shape.width, h: f.shape.height })) { onSelect(f.id); setDrag({ id: f.id, offsetX: pos.x - f.shape.x, offsetY: pos.y - f.shape.y }); return; } } onSelect(null); setDrag(null); }; const handleMouseMove = (e: React.MouseEvent) => { if (!drag) return; const pos = getMousePos(e); if (drag.id === '__entrance__' && state.entrance) { const en = state.entrance; if (drag.resizing) { const newW = Math.max(10, pos.x - en.x); const newH = Math.max(10, pos.y - en.y); onStateChange({ ...state, entrance: { ...en, width: newW, height: newH } }); } else { onStateChange({ ...state, entrance: { ...en, x: pos.x - drag.offsetX, y: pos.y - drag.offsetY }, }); } return; } const updated = state.furniture.map((f) => { if (f.id !== drag.id) return f; return { ...f, shape: { ...f.shape, x: pos.x - drag.offsetX, y: pos.y - drag.offsetY, }, }; }); onStateChange({ ...state, furniture: updated }); }; const handleMouseUp = () => { if (drag && drag.id !== '__entrance__') { const updated = state.furniture.map((f) => (f.id === drag.id ? { ...f, placed: true } : f)); onStateChange({ ...state, furniture: updated }); } setDrag(null); }; return ( ); } function pointInRect(p: Point, r: { x: number; y: number; w: number; h: number }): boolean { return p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h; } function roundRect( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number, ): void { ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }