first commit
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
||||
import { DEFAULT_SIZES, fitScale, isValidPlacement, clampToRoom } from "./geometry";
|
||||
import { initialState, reducer } from "./store";
|
||||
import type { FurnitureType } from "./types";
|
||||
import { remaining, placedCount } from "./inventory";
|
||||
import { Palette, ChairIcon, TableIcon } from "./components/Palette";
|
||||
import { Room, type Preview } from "./components/Room";
|
||||
import { ScaleBar } from "./components/ScaleBar";
|
||||
import { TypeMenu } from "./components/TypeMenu";
|
||||
|
||||
const WALL_PX = 6; // room wall border, must match Room.tsx
|
||||
const SCALE_BAR_PX = 40;
|
||||
|
||||
interface PaletteDrag {
|
||||
type: FurnitureType;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const [paletteDrag, setPaletteDrag] = useState<PaletteDrag | null>(null);
|
||||
const [menu, setMenu] = useState<FurnitureType | null>(null);
|
||||
const closeMenu = useCallback(() => setMenu(null), []);
|
||||
const floorRef = useRef<HTMLDivElement>(null);
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const [pxPerM, setPxPerM] = useState(60);
|
||||
|
||||
// Maximise the room inside the center area (minus wall border and the scale bar).
|
||||
useEffect(() => {
|
||||
const el = mainRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
const { width, height } = entry.contentRect;
|
||||
setPxPerM(fitScale(state.room, width - WALL_PX * 2, height - WALL_PX * 2 - SCALE_BAR_PX));
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [state.room]);
|
||||
const nextId = useRef(1);
|
||||
|
||||
// Cursor position -> room meters (item centered on cursor), or null if outside the floor.
|
||||
const toRoomRect = (d: PaletteDrag) => {
|
||||
const box = floorRef.current?.getBoundingClientRect();
|
||||
if (!box) return null;
|
||||
// Floor excludes the CSS border: the Stage sits inside it.
|
||||
const border = (box.width - state.room.width * pxPerM) / 2;
|
||||
const px = d.clientX - box.left - border;
|
||||
const py = d.clientY - box.top - border;
|
||||
if (px < 0 || py < 0 || px > state.room.width * pxPerM || py > state.room.height * pxPerM) return null;
|
||||
const { width, height } = DEFAULT_SIZES[d.type];
|
||||
return clampToRoom(
|
||||
{ x: px / pxPerM - width / 2, y: py / pxPerM - height / 2, width, height },
|
||||
state.room,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!paletteDrag) return;
|
||||
const type = paletteDrag.type;
|
||||
const move = (e: globalThis.PointerEvent) =>
|
||||
setPaletteDrag({ type, clientX: e.clientX, clientY: e.clientY });
|
||||
const up = (e: globalThis.PointerEvent) => {
|
||||
const r = toRoomRect({ type, clientX: e.clientX, clientY: e.clientY });
|
||||
if (
|
||||
r &&
|
||||
remaining(state.items, type, state.configs[type].available) > 0 &&
|
||||
isValidPlacement(r, state.items, state.room)
|
||||
) {
|
||||
dispatch({ kind: "add", id: `item-${nextId.current++}`, type, x: r.x, y: r.y });
|
||||
} // outside the room or invalid: cancel
|
||||
setPaletteDrag(null);
|
||||
};
|
||||
const cancel = () => setPaletteDrag(null);
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
window.addEventListener("pointercancel", cancel);
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
window.removeEventListener("pointercancel", cancel);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paletteDrag?.type, state.items, state.room, state.configs, pxPerM]);
|
||||
|
||||
const preview: Preview | null = useMemo(() => {
|
||||
if (!paletteDrag) return null;
|
||||
const r = toRoomRect(paletteDrag);
|
||||
if (!r) return null;
|
||||
return { type: paletteDrag.type, ...r, valid: isValidPlacement(r, state.items, state.room) };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paletteDrag, state.items, state.room, pxPerM]);
|
||||
|
||||
const startDrag = (type: FurnitureType, clientX: number, clientY: number) => {
|
||||
setMenu(null);
|
||||
setPaletteDrag({ type, clientX, clientY });
|
||||
};
|
||||
|
||||
const remainingByType = {
|
||||
chair: remaining(state.items, "chair", state.configs.chair.available),
|
||||
table: remaining(state.items, "table", state.configs.table.available),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh", background: "#e9e9e9", cursor: paletteDrag ? "grabbing" : undefined }}>
|
||||
<Palette
|
||||
remaining={remainingByType}
|
||||
onStartDrag={startDrag}
|
||||
onOpenMenu={setMenu}
|
||||
panel={
|
||||
menu && (
|
||||
<TypeMenu
|
||||
type={menu}
|
||||
config={state.configs[menu]}
|
||||
placed={placedCount(state.items, menu)}
|
||||
onSetAvailable={(value) => dispatch({ kind: "setAvailable", type: menu, value })}
|
||||
onSetSeats={(value) => dispatch({ kind: "setSeats", value })}
|
||||
onClose={closeMenu}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<main
|
||||
ref={mainRef}
|
||||
style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", overflow: "hidden", padding: 24 }}
|
||||
>
|
||||
<Room
|
||||
pxPerM={pxPerM}
|
||||
room={state.room}
|
||||
items={state.items}
|
||||
preview={preview}
|
||||
floorRef={floorRef}
|
||||
onMove={(id, x, y) => dispatch({ kind: "move", id, x, y })}
|
||||
/>
|
||||
<ScaleBar pxPerM={pxPerM} roomWidthPx={state.room.width * pxPerM + WALL_PX * 2} />
|
||||
</main>
|
||||
{paletteDrag && !preview && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: paletteDrag.clientX,
|
||||
top: paletteDrag.clientY,
|
||||
transform: "translate(-50%, -50%)",
|
||||
pointerEvents: "none",
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{paletteDrag.type === "chair" ? <ChairIcon /> : <TableIcon />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user