This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:58 +01:00
commit 62a385e4c9
33 changed files with 2649 additions and 809 deletions

View file

@ -1,10 +1,21 @@
import { Plus, Trash2, DoorOpen, Check, MoveUpRight } from 'lucide-react';
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, Issues } from './Fields';
import { NumberField, NumericInput, Issues } from './Fields';
import { DoorPosition } from './DoorPosition';
import { insertWallAfter, moveWall } from '../lib/wall-editing';
interface Props {
room: Room;
geometry: Geometry;
@ -23,6 +34,8 @@ export function OutlineEditor({
remove,
canRemove,
}: Props) {
const draggedWall = useRef<string | null>(null);
const [dropTarget, setDropTarget] = useState<string | null>(null);
const changeWall = (id: string, fn: (wall: Wall) => Wall) =>
changeRoom((r) => ({ ...r, walls: r.walls.map((w) => (w.id === id ? fn(w) : w)) }));
const isRectangle =
@ -85,7 +98,7 @@ export function OutlineEditor({
}));
};
return (
<>
<Fragment key={room.id}>
<div className="editor-heading">
<h2>Room measurements</h2>
</div>
@ -168,23 +181,39 @@ export function OutlineEditor({
<span />
</div>
{room.walls.map((wall, i) => (
<div className="wall-row" key={wall.id}>
<div
className={`wall-row${dropTarget === wall.id ? ' drag-over' : ''}`}
key={wall.id}
data-wall-id={wall.id}
onDragOver={(event) => {
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);
}}
>
<span className="wall-code">W{i + 1}</span>
<input
<NumericInput
aria-label={`Wall ${i + 1} length`}
type="number"
inputMode="decimal"
min={1}
step={1}
value={wall.length}
onChange={(e) =>
changeWall(wall.id, (w) => ({ ...w, length: Number(e.target.value) }))
}
onChange={(length) => changeWall(wall.id, (w) => ({ ...w, length }))}
/>
<input
<NumericInput
aria-label={`Wall ${i + 1} turn`}
type="number"
inputMode="decimal"
min={-179}
max={179}
step={0.1}
@ -199,20 +228,64 @@ export function OutlineEditor({
: 'Turn after this wall; positive turns right.'
}
disabled={geometry.closed && i === room.walls.length - 1}
onChange={(e) =>
changeWall(wall.id, (w) => ({ ...w, turn: Number(e.target.value) }))
}
onChange={(turn) => changeWall(wall.id, (w) => ({ ...w, turn }))}
/>
<button
className="icon-button"
aria-label={`Remove wall ${i + 1}`}
disabled={room.walls.length === 1}
onClick={() =>
changeRoom((r) => ({ ...r, walls: r.walls.filter((w) => w.id !== wall.id) }))
}
>
<Trash2 size={14} />
</button>
<div className="wall-actions">
<button
className="icon-button wall-drag-handle"
draggable
aria-label={`Drag wall ${i + 1} to reorder`}
title="Drag to reorder"
onDragStart={(event) => {
draggedWall.current = wall.id;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', wall.id);
}}
onDragEnd={() => {
draggedWall.current = null;
setDropTarget(null);
}}
>
<GripVertical size={16} />
</button>
<button
className="icon-button"
aria-label={`Move wall ${i + 1} up`}
title="Move wall up"
disabled={i === 0}
onClick={() => changeRoom((r) => moveWall(r, wall.id, i - 1))}
>
<ArrowUp size={16} />
</button>
<button
className="icon-button"
aria-label={`Move wall ${i + 1} down`}
title="Move wall down"
disabled={i === room.walls.length - 1}
onClick={() => changeRoom((r) => moveWall(r, wall.id, i + 1))}
>
<ArrowDown size={16} />
</button>
<button
className="icon-button"
aria-label={`Insert wall after W${i + 1}`}
title={`Insert wall after W${i + 1}`}
disabled={room.walls.length >= 100}
onClick={() => changeRoom((r) => insertWallAfter(r, wall.id))}
>
<Plus size={16} />
</button>
<button
className="icon-button"
aria-label={`Remove wall ${i + 1}`}
disabled={room.walls.length === 1}
onClick={() =>
changeRoom((r) => ({ ...r, walls: r.walls.filter((w) => w.id !== wall.id) }))
}
>
<Trash2 size={14} />
</button>
</div>
</div>
))}
</div>
@ -364,6 +437,6 @@ export function OutlineEditor({
Remove this room
</button>
)}
</>
</Fragment>
);
}