..
This commit is contained in:
parent
9689bfcc18
commit
62a385e4c9
33 changed files with 2649 additions and 809 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue