first commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { useState, type RefObject } from "react";
|
||||
import { Stage, Layer, Group } from "react-konva";
|
||||
import type Konva from "konva";
|
||||
import { isValidPlacement, clampToRoom } from "../geometry";
|
||||
import type { FurnitureItem, FurnitureType, Room as RoomT } from "../types";
|
||||
import { FurnitureShape } from "./FurnitureShape";
|
||||
|
||||
export interface Preview {
|
||||
type: FurnitureType;
|
||||
x: number; // meters
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
room: RoomT;
|
||||
items: FurnitureItem[];
|
||||
preview: Preview | null; // new item being dragged in from the palette
|
||||
pxPerM: number;
|
||||
floorRef: RefObject<HTMLDivElement | null>;
|
||||
onMove: (id: string, x: number, y: number) => void;
|
||||
}
|
||||
|
||||
interface DragInfo {
|
||||
id: string;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export function Room({ room, items, preview, pxPerM, floorRef, onMove }: Props) {
|
||||
const [drag, setDrag] = useState<DragInfo | null>(null);
|
||||
const w = room.width * pxPerM;
|
||||
const h = room.height * pxPerM;
|
||||
|
||||
// Where the item would land if dropped now: clamped to walls.
|
||||
const candidate = (item: FurnitureItem, node: Konva.Node) =>
|
||||
clampToRoom(
|
||||
{ x: node.x() / pxPerM, y: node.y() / pxPerM, width: item.width, height: item.height },
|
||||
room,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={floorRef}
|
||||
style={{
|
||||
width: w,
|
||||
height: h,
|
||||
border: "6px solid #2b2b2b",
|
||||
boxSizing: "content-box",
|
||||
background: "#fff",
|
||||
boxShadow: "0 4px 18px rgba(0,0,0,0.12)",
|
||||
touchAction: "none",
|
||||
}}
|
||||
>
|
||||
<Stage width={w} height={h}>
|
||||
<Layer>
|
||||
{items.map((item) => {
|
||||
const outline =
|
||||
drag?.id === item.id ? (drag.valid ? "valid" : "invalid") : "normal";
|
||||
return (
|
||||
<Group
|
||||
key={item.id}
|
||||
x={item.x * pxPerM}
|
||||
y={item.y * pxPerM}
|
||||
draggable={!item.locked}
|
||||
// Hard-clamp to the walls while dragging (stage is unscaled, so absolute = local).
|
||||
dragBoundFunc={(pos) => ({
|
||||
x: Math.min(Math.max(pos.x, 0), w - item.width * pxPerM),
|
||||
y: Math.min(Math.max(pos.y, 0), h - item.height * pxPerM),
|
||||
})}
|
||||
onDragStart={(e) => {
|
||||
e.target.moveToTop();
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
const c = candidate(item, e.target);
|
||||
setDrag({ id: item.id, valid: isValidPlacement(c, items, room, item.id) });
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
const node = e.target;
|
||||
const c = candidate(item, node);
|
||||
const ok = isValidPlacement(c, items, room, item.id);
|
||||
const final = ok ? c : item; // snap back to last valid position
|
||||
node.position({ x: final.x * pxPerM, y: final.y * pxPerM });
|
||||
if (ok) onMove(item.id, c.x, c.y);
|
||||
setDrag(null);
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const c = e.target.getStage()?.container();
|
||||
if (c) c.style.cursor = "grab";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const c = e.target.getStage()?.container();
|
||||
if (c) c.style.cursor = "";
|
||||
}}
|
||||
>
|
||||
<FurnitureShape type={item.type} width={item.width} height={item.height} pxPerM={pxPerM} outline={outline} />
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
{preview && (
|
||||
<Group x={preview.x * pxPerM} y={preview.y * pxPerM} listening={false}>
|
||||
<FurnitureShape
|
||||
type={preview.type}
|
||||
width={preview.width}
|
||||
height={preview.height}
|
||||
pxPerM={pxPerM}
|
||||
outline={preview.valid ? "valid" : "invalid"}
|
||||
opacity={0.85}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user