import { Fragment, useRef, useState } from 'react'; import { Plus, Trash2, DoorOpen, Check, MoveUpRight, GripVertical, ArrowUp, ArrowDown, } from 'lucide-react'; import type { Geometry, Project, Room, Wall } from '../lib/types'; import { fireplace, rectangle, nextId } from '../lib/defaults'; import { normalizeAngle } from '../lib/geometry'; import { mm } from '../lib/format'; import { NumberField, NumericInput, Issues } from './Fields'; import { DoorPosition } from './DoorPosition'; import { insertWallAfter, moveWall } from '../lib/wall-editing'; import { PRECISION, PROJECT_LIMITS } from '../lib/limits'; interface Props { room: Room; geometry: Geometry; tolerance: number; changeRoom: (fn: (room: Room) => Room) => void; changeProject: (fn: (project: Project) => Project) => void; remove: () => void; canRemove: boolean; } export function OutlineEditor({ room, geometry, tolerance, changeRoom, changeProject, remove, canRemove, }: Props) { const draggedWall = useRef(null); const [dropTarget, setDropTarget] = useState(null); const [customSelected, setCustomSelected] = useState(false); const changeWall = (id: string, fn: (wall: Wall) => Wall) => changeRoom((r) => ({ ...r, walls: r.walls.map((w) => (w.id === id ? fn(w) : w)) })); const isRectangle = room.walls.length === 4 && room.walls.every((w) => w.turn === 90) && room.walls[0].length === room.walls[2].length && room.walls[1].length === room.walls[3].length; const isFireplace = room.walls.length === 8 && room.walls.every((wall, index) => wall.turn === [90, 90, 90, -90, -90, 90, 90, 90][index]); const isCustom = customSelected || (!isRectangle && !isFireplace); const lastEdge = geometry.edges.at(-1); const finalExpectedLength = lastEdge ? Math.hypot(lastEdge.start.x, lastEdge.start.y) : 0; const addWall = () => changeRoom((r) => ({ ...r, walls: [ ...r.walls, { id: nextId( `${r.id}-w`, r.walls.map((w) => w.id), ), length: 1000, turn: 90, doors: [], }, ], })); const closeOutline = () => changeRoom((r) => ({ ...r, walls: [ ...r.walls.map((w, i) => i === r.walls.length - 1 ? { ...w, turn: geometry.closingTurn } : w, ), { id: nextId( `${r.id}-w`, r.walls.map((w) => w.id), ), length: Math.round(geometry.closingLength * 10) / 10, turn: 90, doors: [], }, ], })); const addDoor = () => { const target = room.walls[room.walls.length - 1]; if (!target) return; changeWall(target.id, (w) => ({ ...w, doors: [ ...w.doors, { id: nextId( `${room.id}-door`, room.walls.flatMap((w) => w.doors.map((d) => d.id)), ), offset: Math.min(500, Math.max(0, w.length - 826)), width: 826, }, ], })); }; return (

Room measurements

Room shape

{isCustom && geometry.closed ? ( <>Your outline is complete. Adjust individual walls, corners or doorways below. ) : isCustom ? ( <> Add your walls in order below, then use Add closing wall to complete the outline. ) : ( <> Have an irregular room? Click Custom to start a new outline and enter each wall and corner. Choosing a shape replaces the current outline and doorways. )}

{isRectangle && !isCustom && (
changeRoom((r) => ({ ...r, walls: r.walls.map((w, i) => (i % 2 === 0 ? { ...w, length: n } : w)), })) } /> changeRoom((r) => ({ ...r, walls: r.walls.map((w, i) => (i % 2 === 1 ? { ...w, length: n } : w)), })) } />
)}
Walls & corners {room.walls.length} walls

Start to the right; work clockwise. Right turn: 90°. Left turn: −90°.

Wall Length · mm Next turn · °
{room.walls.map((wall, i) => (
{ if (!draggedWall.current || draggedWall.current === wall.id) return; event.preventDefault(); event.dataTransfer.dropEffect = 'move'; setDropTarget(wall.id); }} onDragLeave={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDropTarget(null); }} onDrop={(event) => { const id = draggedWall.current; if (!id) return; event.preventDefault(); changeRoom((r) => moveWall(r, id, i)); draggedWall.current = null; setDropTarget(null); }} > W{i + 1} changeWall(wall.id, (w) => ({ ...w, length }))} /> changeWall(wall.id, (w) => ({ ...w, turn }))} />
))}
{geometry.closed ? (
Outline closed Closing gap {mm(geometry.gap)} mm · tolerance {mm(tolerance)} mm Final wall: {mm(finalExpectedLength)} mm expected ·{' '} {mm(Math.abs(geometry.finalLengthError))} mm difference
) : ( room.walls.length >= 2 && geometry.closingLength > 0 && (
{mm(geometry.closingLength)} mm to close

Next turn {mm(geometry.closingTurn)}°. After adding, enter the measured length.

{room.walls.length >= 3 && (

If W{room.walls.length} is the final wall: {mm(finalExpectedLength)} mm expected; length differs by {mm(Math.abs(geometry.finalLengthError))} mm, direction by{' '} {mm(Math.abs(geometry.finalAngleError))}°.

)}
) )} i.severity !== 'info')} compact />

Doorways

{room.walls.reduce((n, w) => n + w.doors.length, 0)} openings

Door width includes trim where skirting stops.

{room.walls.flatMap((wall, wallIndex) => wall.doors.map((door) => (
changeWall(wall.id, (w) => ({ ...w, doors: w.doors.map((d) => (d.id === door.id ? { ...d, offset: n } : d)), })) } /> changeWall(wall.id, (w) => ({ ...w, doors: w.doors.map((d) => (d.id === door.id ? { ...d, width: n } : d)), })) } />
)), )}
changeProject((p) => ({ ...p, tolerance: n }))} hint="Largest closing gap accepted for the cut plan." /> {canRemove && ( )} ); }