second commit

This commit is contained in:
2026-10-17 12:30:46 -04:00
parent 56046a63fc
commit abc3800db2
14 changed files with 533 additions and 136 deletions
+90 -11
View File
@@ -1,8 +1,8 @@
import { useState, type RefObject } from "react";
import { Stage, Layer, Group } from "react-konva";
import { useRef, useState, type RefObject } from "react";
import { Stage, Layer, Group, Rect } from "react-konva";
import type Konva from "konva";
import { isValidPlacement, clampToRoom } from "../geometry";
import type { FurnitureItem, FurnitureType, Room as RoomT } from "../types";
import { MIN_SCREEN_SIZE, RESIZABLE, isValidPlacement, clampToRoom, resizeRect } from "../geometry";
import type { FurnitureItem, FurnitureType, Rect as RectM, Room as RoomT } from "../types";
import { FurnitureShape } from "./FurnitureShape";
export interface Preview {
@@ -21,6 +21,7 @@ interface Props {
pxPerM: number;
floorRef: RefObject<HTMLDivElement | null>;
onMove: (id: string, x: number, y: number) => void;
onResize: (id: string, rect: RectM) => void;
}
interface DragInfo {
@@ -28,8 +29,21 @@ interface DragInfo {
valid: boolean;
}
export function Room({ room, items, preview, pxPerM, floorRef, onMove }: Props) {
interface ResizeInfo {
id: string;
rect: RectM;
valid: boolean;
}
const HANDLE_PX = 10;
// Top-down view: a screen only changes in length, its thickness is fixed.
const EDGES: ("left" | "right")[] = ["left", "right"];
export function Room({ room, items, preview, pxPerM, floorRef, onMove, onResize }: Props) {
const [drag, setDrag] = useState<DragInfo | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [resize, setResize] = useState<ResizeInfo | null>(null);
const resizeStart = useRef<RectM | null>(null);
const w = room.width * pxPerM;
const h = room.height * pxPerM;
@@ -53,16 +67,28 @@ export function Room({ room, items, preview, pxPerM, floorRef, onMove }: Props)
touchAction: "none",
}}
>
<Stage width={w} height={h}>
<Stage
width={w}
height={h}
onMouseDown={(e) => e.target === e.target.getStage() && setSelectedId(null)}
onTouchStart={(e) => e.target === e.target.getStage() && setSelectedId(null)}
>
<Layer>
{items.map((item) => {
const outline =
drag?.id === item.id ? (drag.valid ? "valid" : "invalid") : "normal";
const isResizing = resize?.id === item.id;
const r: RectM = isResizing ? resize.rect : item;
const outline = isResizing
? resize.valid ? "valid" : "invalid"
: drag?.id === item.id
? drag.valid ? "valid" : "invalid"
: selectedId === item.id ? "selected" : "normal";
return (
<Group
key={item.id}
x={item.x * pxPerM}
y={item.y * pxPerM}
x={r.x * pxPerM}
y={r.y * pxPerM}
onMouseDown={() => setSelectedId(RESIZABLE[item.type] ? item.id : null)}
onTouchStart={() => setSelectedId(RESIZABLE[item.type] ? item.id : null)}
draggable={!item.locked}
// Hard-clamp to the walls while dragging (stage is unscaled, so absolute = local).
dragBoundFunc={(pos) => ({
@@ -94,10 +120,63 @@ export function Room({ room, items, preview, pxPerM, floorRef, onMove }: Props)
if (c) c.style.cursor = "";
}}
>
<FurnitureShape type={item.type} width={item.width} height={item.height} pxPerM={pxPerM} outline={outline} />
<FurnitureShape type={item.type} width={r.width} height={r.height} pxPerM={pxPerM} outline={outline} />
</Group>
);
})}
{items.map((item) => {
if (item.id !== selectedId || !RESIZABLE[item.type] || drag?.id === item.id) return null;
const r: RectM = resize?.id === item.id ? resize.rect : item;
const mid: Record<"left" | "right", readonly [number, number]> = {
left: [r.x, r.y + r.height / 2],
right: [r.x + r.width, r.y + r.height / 2],
};
return EDGES.map((edge) => (
<Rect
key={`${item.id}-${edge}`}
x={mid[edge][0] * pxPerM}
y={mid[edge][1] * pxPerM}
offsetX={HANDLE_PX / 2}
offsetY={HANDLE_PX / 2}
width={HANDLE_PX}
height={HANDLE_PX}
fill="#fff"
stroke="#1c7ed6"
strokeWidth={2}
cornerRadius={2}
hitStrokeWidth={12}
draggable
// The handle is pinned in place; the pointer drives the new size, React moves the handle.
dragBoundFunc={function (this: Konva.Node) {
return this.absolutePosition();
}}
onDragStart={(e) => {
e.cancelBubble = true;
resizeStart.current = { x: item.x, y: item.y, width: item.width, height: item.height };
}}
onDragMove={(e) => {
const p = e.target.getStage()?.getPointerPosition();
const start = resizeStart.current;
if (!p || !start) return;
const next = resizeRect(start, edge, p.x / pxPerM, p.y / pxPerM, room, MIN_SCREEN_SIZE);
setResize({ id: item.id, rect: next, valid: isValidPlacement(next, items, room, item.id) });
}}
onDragEnd={() => {
if (resize?.valid) onResize(item.id, resize.rect); // else snap back to the old size
setResize(null);
resizeStart.current = null;
}}
onMouseEnter={(e) => {
const c = e.target.getStage()?.container();
if (c) c.style.cursor = "ew-resize";
}}
onMouseLeave={(e) => {
const c = e.target.getStage()?.container();
if (c) c.style.cursor = "";
}}
/>
));
})}
{preview && (
<Group x={preview.x * pxPerM} y={preview.y * pxPerM} listening={false}>
<FurnitureShape