second commit
This commit is contained in:
+90
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user