first commit

This commit is contained in:
2026-10-17 12:09:36 -04:00
commit 56046a63fc
1149 changed files with 658055 additions and 0 deletions
+118
View File
@@ -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>
);
}