This commit is contained in:
Andras Schmelczer 2026-09-11 22:14:24 +01:00
commit 0c0f08b350
45 changed files with 1565 additions and 1579 deletions

View file

@ -16,6 +16,7 @@ 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;
@ -36,6 +37,7 @@ export function OutlineEditor({
}: Props) {
const draggedWall = useRef<string | null>(null);
const [dropTarget, setDropTarget] = useState<string | null>(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 =
@ -43,6 +45,10 @@ export function OutlineEditor({
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 = () =>
@ -106,41 +112,72 @@ export function OutlineEditor({
<span>Room name</span>
<input
value={room.name}
maxLength={80}
maxLength={PROJECT_LIMITS.nameLength}
onChange={(e) => changeRoom((r) => ({ ...r, name: e.target.value }))}
/>
</label>
<div className="template-options">
<div className="subheading shape-heading">
<h3>Room shape</h3>
</div>
<div
className="template-options"
role="group"
aria-label="Room shape"
aria-describedby="custom-shape-help"
>
<button
className={isRectangle ? 'template active' : 'template'}
aria-pressed={isRectangle}
onClick={() => changeRoom((r) => rectangle(r.id, r.name))}
className={isRectangle && !isCustom ? 'template active' : 'template'}
aria-pressed={isRectangle && !isCustom}
onClick={() => {
setCustomSelected(false);
changeRoom((r) => rectangle(r.id, r.name));
}}
>
<span className="rectangle-icon" />
Rectangle
</button>
<button
className={room.walls.length === 8 ? 'template active' : 'template'}
aria-pressed={room.walls.length === 8}
onClick={() => changeRoom((r) => fireplace(r.id, r.name))}
className={isFireplace && !isCustom ? 'template active' : 'template'}
aria-pressed={isFireplace && !isCustom}
onClick={() => {
setCustomSelected(false);
changeRoom((r) => fireplace(r.id, r.name));
}}
>
<span className="fireplace-icon" />
With fireplace
</button>
<button
className="template"
onClick={() =>
className={isCustom ? 'template active' : 'template'}
aria-pressed={isCustom}
onClick={() => {
setCustomSelected(true);
changeRoom((r) => ({
...r,
walls: [{ id: `${r.id}-w1`, length: 4000, turn: 90, doors: [] }],
}))
}
}));
}}
>
<MoveUpRight size={19} />
Custom
</button>
</div>
{isRectangle && (
<p className="shape-help" id="custom-shape-help">
{isCustom && geometry.closed ? (
<>Your outline is complete. Adjust individual walls, corners or doorways below.</>
) : isCustom ? (
<>
Add your walls in order below, then use <strong>Add closing wall</strong> to complete
the outline.
</>
) : (
<>
Have an irregular room? Click <strong>Custom</strong> to start a new outline and enter
each wall and corner. Choosing a shape replaces the current outline and doorways.
</>
)}
</p>
{isRectangle && !isCustom && (
<div className="field-grid quick-dimensions">
<NumberField
label="Room width"
@ -166,7 +203,7 @@ export function OutlineEditor({
/>
</div>
)}
<details className="wall-details" open={!isRectangle}>
<details className="wall-details" open={isCustom || !isRectangle}>
<summary>
Walls & corners <span>{room.walls.length} walls</span>
</summary>
@ -270,7 +307,7 @@ export function OutlineEditor({
className="icon-button"
aria-label={`Insert wall after W${i + 1}`}
title={`Insert wall after W${i + 1}`}
disabled={room.walls.length >= 100}
disabled={room.walls.length >= PROJECT_LIMITS.wallsPerRoom}
onClick={() => changeRoom((r) => insertWallAfter(r, wall.id))}
>
<Plus size={16} />
@ -289,7 +326,11 @@ export function OutlineEditor({
</div>
))}
</div>
<button className="text-button" onClick={addWall} disabled={room.walls.length >= 100}>
<button
className="text-button"
onClick={addWall}
disabled={room.walls.length >= PROJECT_LIMITS.wallsPerRoom}
>
<Plus size={15} /> Add a wall
</button>
</details>
@ -317,7 +358,10 @@ export function OutlineEditor({
<button
className="text-button"
onClick={closeOutline}
disabled={room.walls.length >= 100 || Math.abs(geometry.closingTurn) >= 179.99}
disabled={
room.walls.length >= PROJECT_LIMITS.wallsPerRoom ||
Math.abs(geometry.closingTurn) >= PROJECT_LIMITS.maxTurn - PRECISION.millimetres
}
>
Add closing wall <Plus size={14} />
</button>
@ -419,7 +463,9 @@ export function OutlineEditor({
<button
className="text-button"
onClick={addDoor}
disabled={!room.walls.length || room.walls.at(-1)!.doors.length >= 30}
disabled={
!room.walls.length || room.walls.at(-1)!.doors.length >= PROJECT_LIMITS.doorsPerWall
}
>
<Plus size={15} /> Add a doorway
</button>
@ -427,7 +473,7 @@ export function OutlineEditor({
<NumberField
label="Measurement tolerance"
value={tolerance}
max={50}
max={PROJECT_LIMITS.maxTolerance}
onChange={(n) => changeProject((p) => ({ ...p, tolerance: n }))}
hint="Largest closing gap accepted for the cut plan."
/>