item catalog draft
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import useEditor from "@/store/use-editor";
|
||||
import {
|
||||
emitter,
|
||||
GridEvent,
|
||||
@@ -17,8 +18,12 @@ export const ItemTool: React.FC = () => {
|
||||
const cursorRef = useRef<Mesh>(null);
|
||||
const draftItem = useRef<ItemNode | null>(null);
|
||||
const gridPosition = useRef(new Vector3(0, 0, 0));
|
||||
const selectedItem = useEditor((state) => state.selectedItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedItem) {
|
||||
return;
|
||||
}
|
||||
const createDraftItem = () => {
|
||||
const { currentLevelId } = useViewer.getState();
|
||||
if (!currentLevelId) {
|
||||
@@ -26,13 +31,13 @@ export const ItemTool: React.FC = () => {
|
||||
}
|
||||
useScene.temporal.getState().pause();
|
||||
draftItem.current = ItemNode.parse({
|
||||
position: [randInt(-10, 10), 0, randInt(-10, 10)],
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
name: "Draft Item",
|
||||
asset: {
|
||||
category: "furniture",
|
||||
src: "/items/couch-small/model.glb",
|
||||
dimensions: [2, 1, 3],
|
||||
},
|
||||
asset: selectedItem,
|
||||
});
|
||||
useScene.getState().createNode(draftItem.current, currentLevelId);
|
||||
};
|
||||
@@ -68,7 +73,11 @@ export const ItemTool: React.FC = () => {
|
||||
console.log("oh");
|
||||
useScene.temporal.getState().resume();
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
position: [gridPosition.current.x, 0, gridPosition.current.z],
|
||||
position: [
|
||||
gridPosition.current.x,
|
||||
gridPosition.current.y,
|
||||
gridPosition.current.z,
|
||||
],
|
||||
});
|
||||
draftItem.current = null;
|
||||
|
||||
@@ -86,13 +95,13 @@ export const ItemTool: React.FC = () => {
|
||||
emitter.off("grid:move", onGridMove);
|
||||
emitter.off("grid:click", onGridClick);
|
||||
};
|
||||
}, []);
|
||||
}, [selectedItem]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (draftItem.current) {
|
||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id);
|
||||
if (draftItemMesh) {
|
||||
draftItemMesh.position.lerp(gridPosition.current, delta * 10);
|
||||
draftItemMesh.position.lerp(gridPosition.current, delta * 20);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,12 +8,14 @@ import { PhaseSwitcher } from "./phase-switcher";
|
||||
import { StructureTools } from "./structure-tools";
|
||||
import useEditor from "@/store/use-editor";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { ItemCatalog } from "../item-catalog/item-catalog";
|
||||
// import { ViewToggles } from "./view-toggles";
|
||||
|
||||
export function ActionMenu({ className }: { className?: string }) {
|
||||
|
||||
const phase = useEditor((state) => state.phase);
|
||||
const mode = useEditor((state) => state.mode);
|
||||
const tool = useEditor((state) => state.tool);
|
||||
const catalogCategory = useEditor((state) => state.catalogCategory);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
@@ -22,26 +24,77 @@ export function ActionMenu({ className }: { className?: string }) {
|
||||
"-translate-x-1/2 fixed bottom-6 left-1/2 z-50",
|
||||
"rounded-2xl border border-zinc-800 bg-zinc-950/90 shadow-2xl backdrop-blur-md",
|
||||
"transition-all duration-200 ease-out",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Structure Tools Row - Animated */}
|
||||
<AnimatePresence>
|
||||
{tool === "item" && catalogCategory && (
|
||||
<motion.div
|
||||
className={cn(
|
||||
"overflow-hidden border-zinc-800 max-h-96 border-b px-2 py-2",
|
||||
)}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
maxHeight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
borderBottomWidth: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
maxHeight: 80,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
borderBottomWidth: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
maxHeight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
borderBottomWidth: 0,
|
||||
}}
|
||||
>
|
||||
<ItemCatalog category={catalogCategory} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Structure Tools Row - Animated */}
|
||||
<AnimatePresence>
|
||||
{phase === "structure" && mode === "build" && (
|
||||
<motion.div
|
||||
className={cn(
|
||||
'overflow-hidden border-zinc-800 max-h-20 border-b px-2 py-2'
|
||||
)}
|
||||
initial={{ opacity: 0, maxHeight: 0, paddingTop: 0, paddingBottom: 0,borderBottomWidth: 0, }}
|
||||
animate={{ opacity: 1, maxHeight: 80, paddingTop: 8, paddingBottom: 8, borderBottomWidth: 1, }}
|
||||
exit={{ opacity: 0, maxHeight: 0, paddingTop: 0, paddingBottom: 0, borderBottomWidth: 0, }}
|
||||
>
|
||||
<div className="w-max">
|
||||
<StructureTools />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{phase === "structure" && mode === "build" && (
|
||||
<motion.div
|
||||
className={cn(
|
||||
"overflow-hidden border-zinc-800 max-h-20 border-b px-2 py-2",
|
||||
)}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
maxHeight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
borderBottomWidth: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
maxHeight: 80,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
borderBottomWidth: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
maxHeight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
borderBottomWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-max">
|
||||
<StructureTools />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Control Mode Row - Always visible, centered */}
|
||||
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import { AssetInput, ItemNode } from "@pascal-app/core";
|
||||
export const CATALOG_ITEMS: AssetInput[] = [
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Couch",
|
||||
thumbnail: "/items/couch-medium/thumbnail.webp",
|
||||
src: "/items/couch-medium/model.glb",
|
||||
scale: [0.4, 0.4, 0.4],
|
||||
dimensions: [4, 2, 2],
|
||||
},
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Small Couch",
|
||||
thumbnail: "/items/couch-small/thumbnail.webp",
|
||||
src: "/items/couch-small/model.glb",
|
||||
scale: [0.4, 0.4, 0.4],
|
||||
dimensions: [3, 2, 2],
|
||||
},
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Desk",
|
||||
thumbnail: "/items/desk/thumbnail.webp",
|
||||
src: "/items/desk/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
dimensions: [4, 2, 2],
|
||||
},
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Table",
|
||||
thumbnail: "/items/table/thumbnail.webp",
|
||||
src: "/items/table/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI / 2, 0],
|
||||
dimensions: [6, 2, 3],
|
||||
},
|
||||
{
|
||||
category: "outdoor",
|
||||
name: "Fence",
|
||||
thumbnail: "/items/fence/thumbnail.webp",
|
||||
src: "/items/fence/model.glb",
|
||||
scale: [0.8, 0.8, 0.8],
|
||||
offset: [0, 0, -0.12],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [6, 2, 1],
|
||||
},
|
||||
{
|
||||
category: "outdoor",
|
||||
name: "Parking Spot",
|
||||
thumbnail: "/items/parking-spot/thumbnail.webp",
|
||||
src: "/items/parking-spot/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [12, 2, 6],
|
||||
},
|
||||
{
|
||||
category: "outdoor",
|
||||
name: "Hedge",
|
||||
thumbnail: "/items/hedge/thumbnail.webp",
|
||||
src: "/items/hedge/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [4, 2, 3],
|
||||
},
|
||||
{
|
||||
category: "outdoor",
|
||||
name: "Tree",
|
||||
thumbnail: "/items/tree/thumbnail.webp",
|
||||
src: "/items/tree/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 2, 2],
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Ceiling Light",
|
||||
thumbnail: "/items/ceiling-light/thumbnail.webp",
|
||||
src: "/items/ceiling-light/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Ceiling Fan",
|
||||
thumbnail: "/items/ceiling-fan/thumbnail.webp",
|
||||
src: "/items/ceiling-fan/model.glb",
|
||||
scale: [0.003, 0.003, 0.003],
|
||||
offset: [0, -0.62, 0],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Rectangular Ceiling Light",
|
||||
thumbnail: "/items/rectangular-ceiling-light/thumbnail.webp",
|
||||
src: "/items/rectangular-ceiling-light/model.glb",
|
||||
scale: [0.04, 0.04, 0.04],
|
||||
offset: [0.2, -0.1, 0.95],
|
||||
rotation: [0, Math.PI / 2, 0],
|
||||
dimensions: [2, 2, 4],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Circular Ceiling Light",
|
||||
thumbnail: "/items/circular-ceiling-light/thumbnail.webp",
|
||||
src: "/items/circular-ceiling-light/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
offset: [0, -0.1, 0],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Smoke Detector",
|
||||
thumbnail: "/items/smoke-detector/thumbnail.webp",
|
||||
src: "/items/smoke-detector/model.glb",
|
||||
scale: [1, 1, 1],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [Math.PI, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Sprinkler",
|
||||
thumbnail: "/items/sprinkler/thumbnail.webp",
|
||||
src: "/items/sprinkler/model.glb",
|
||||
scale: [3, 3, 3],
|
||||
offset: [0, 0, 0],
|
||||
rotation: [Math.PI, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Exit Sign",
|
||||
thumbnail: "/items/exit-sign/thumbnail.webp",
|
||||
src: "/items/exit-sign/model.glb",
|
||||
scale: [0.5, 0.5, 0.5],
|
||||
offset: [0, -0.3, 0],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "ceiling",
|
||||
},
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Wall Art",
|
||||
thumbnail: "/items/wall-art-06/thumbnail.webp",
|
||||
src: "/items/wall-art-06/model.glb",
|
||||
offset: [0, 1, 0.15],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "furniture",
|
||||
name: "Flat Screen TV",
|
||||
thumbnail: "/items/flat-screen-tv/thumbnail.webp",
|
||||
src: "/items/flat-screen-tv/model.glb",
|
||||
offset: [-0.2, 1, 0.15],
|
||||
scale: [0.42, 0.42, 0.42],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [4, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Air Conditioner",
|
||||
thumbnail: "/items/air-conditioner/thumbnail.webp",
|
||||
src: "/items/air-conditioner/model.glb",
|
||||
offset: [0, 2, 0.3],
|
||||
scale: [0.0015, 0.0015, 0.0015],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [3, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "AC Block",
|
||||
thumbnail: "/items/ac-block/thumbnail.webp",
|
||||
src: "/items/ac-block/model.glb",
|
||||
offset: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [3, 2, 3],
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Fire extinguisher",
|
||||
thumbnail: "/items/fire-extinguisher/thumbnail.webp",
|
||||
src: "/items/fire-extinguisher/model.glb",
|
||||
offset: [0, 1.5, 0.22],
|
||||
scale: [0.8, 0.8, 0.8],
|
||||
rotation: [0, -Math.PI / 2, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Fire alarm ",
|
||||
thumbnail: "/items/fire-alarm/thumbnail.webp",
|
||||
src: "/items/fire-alarm/model.glb",
|
||||
offset: [0, 1, 0.14],
|
||||
scale: [0.15, 0.15, 0.15],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Fire detector ",
|
||||
thumbnail: "/items/fire-detector/thumbnail.webp",
|
||||
src: "/items/fire-detector/model.glb",
|
||||
offset: [0, 1.1, 0.14],
|
||||
scale: [2, 2, 2],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Alarm keypad",
|
||||
thumbnail: "/items/alarm-keypad/thumbnail.webp",
|
||||
src: "/items/alarm-keypad/model.glb",
|
||||
offset: [0, 1.2, 0.1],
|
||||
scale: [2, 2, 2],
|
||||
rotation: [Math.PI / 2, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Thermostat",
|
||||
thumbnail: "/items/thermostat/thumbnail.webp",
|
||||
src: "/items/thermostat/model.glb",
|
||||
offset: [0, 1.2, 0.1],
|
||||
scale: [5, 5, 5],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Electric panel",
|
||||
thumbnail: "/items/electric-panel/thumbnail.webp",
|
||||
src: "/items/electric-panel/model.glb",
|
||||
offset: [0, 0.4, 0.16],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "appliance",
|
||||
name: "Air Conditioner Block",
|
||||
thumbnail: "/items/air-conditioner-block/thumbnail.webp",
|
||||
src: "/items/air-conditioner-block/model.glb",
|
||||
offset: [0, 0.5, -0.06],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [2, 2, 2],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "kitchen",
|
||||
name: "Kitchen Fridge",
|
||||
thumbnail: "/items/kitchen-fridge/thumbnail.webp",
|
||||
src: "/items/kitchen-fridge/model.glb",
|
||||
offset: [0, 0, 0.0],
|
||||
scale: [0.7, 0.7, 0.7],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [2, 2, 2],
|
||||
},
|
||||
{
|
||||
category: "kitchen",
|
||||
name: "Freezer",
|
||||
thumbnail: "/items/freezer/thumbnail.webp",
|
||||
src: "/items/freezer/model.glb",
|
||||
offset: [-0.43, 0, 0.25],
|
||||
scale: [1.8, 1.8, 1.8],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
},
|
||||
{
|
||||
category: "bathroom",
|
||||
name: "Wall Sink",
|
||||
thumbnail: "/items/wall-sink/thumbnail.webp",
|
||||
src: "/items/wall-sink/model.glb",
|
||||
offset: [0, 0, 0.25],
|
||||
scale: [0.7, 0.7, 0.7],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall-side",
|
||||
},
|
||||
{
|
||||
category: "kitchen",
|
||||
name: "Sink with cabinet",
|
||||
thumbnail: "/items/sink-cabinet/thumbnail.webp",
|
||||
src: "/items/sink-cabinet/model.glb",
|
||||
offset: [0, 0.5, 0],
|
||||
scale: [1.4, 1.4, 1.4],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [4, 2, 1],
|
||||
},
|
||||
{
|
||||
category: "bathroom",
|
||||
name: "Toilet",
|
||||
thumbnail: "/items/toilet/thumbnail.webp",
|
||||
src: "/items/toilet/model.glb",
|
||||
offset: [0, 0, -0.1],
|
||||
scale: [0.005, 0.005, 0.005],
|
||||
rotation: [0, -Math.PI / 4, 0],
|
||||
dimensions: [1, 2, 1],
|
||||
},
|
||||
{
|
||||
category: "window",
|
||||
name: "Window",
|
||||
thumbnail: "/items/window-small/thumbnail.webp",
|
||||
src: "/items/window-small/model.glb",
|
||||
offset: [0, 0.5, 0],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "window",
|
||||
name: "Window",
|
||||
thumbnail: "/items/window-small-2/thumbnail.webp",
|
||||
src: "/items/window-small-2/model.glb",
|
||||
offset: [0, 0.5, 0],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "window",
|
||||
name: "Window",
|
||||
thumbnail: "/items/window-round/thumbnail.webp",
|
||||
src: "/items/window-round/model.glb",
|
||||
offset: [0, 0.5, 0],
|
||||
scale: [1, 1, 1],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "window",
|
||||
name: "Window",
|
||||
thumbnail: "/items/window1-black-open-1731/thumbnail.webp",
|
||||
src: "/items/window1-black-open-1731/model.glb",
|
||||
offset: [0, 0.5, 0],
|
||||
scale: [0.4, 0.4, 0.4],
|
||||
rotation: [0, 0, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "door",
|
||||
name: "Door",
|
||||
thumbnail: "/items/doorway-front/thumbnail.webp",
|
||||
src: "/items/doorway-front/model.glb",
|
||||
offset: [0, 0, 0],
|
||||
scale: [2, 2, 2],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "door",
|
||||
name: "Door",
|
||||
thumbnail: "/items/door/thumbnail.webp",
|
||||
src: "/items/door/model.glb",
|
||||
offset: [0, 0, 0],
|
||||
scale: [0.2, 0.2, 0.2],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
{
|
||||
category: "door",
|
||||
name: "Door with bar",
|
||||
thumbnail: "/items/door-with-bar/thumbnail.webp",
|
||||
src: "/items/door-with-bar/model.glb",
|
||||
offset: [0, 0, 0],
|
||||
scale: [0.65, 0.65, 0.65],
|
||||
rotation: [0, Math.PI, 0],
|
||||
dimensions: [2, 1, 1],
|
||||
attachTo: "wall",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/primitives/tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import useEditor, { CatalogCategory } from "@/store/use-editor";
|
||||
import { CATALOG_ITEMS } from "./catalog-items";
|
||||
import { AssetInput } from "@pascal-app/core";
|
||||
|
||||
export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
||||
const selectedItem = useEditor((state) => state.selectedItem);
|
||||
const setSelectedItem = useEditor((state) => state.setSelectedItem);
|
||||
|
||||
const filteredItems = CATALOG_ITEMS.filter(
|
||||
(item) => item.category === category,
|
||||
);
|
||||
|
||||
// Auto-select first item if current selection is not in the filtered list
|
||||
useEffect(() => {
|
||||
const isCurrentItemInCategory = filteredItems.some(
|
||||
(item) => item.src === selectedItem?.src,
|
||||
);
|
||||
if (!isCurrentItemInCategory && filteredItems.length > 0) {
|
||||
setSelectedItem(filteredItems[0] as AssetInput);
|
||||
}
|
||||
}, [filteredItems, selectedItem?.src, setSelectedItem]);
|
||||
|
||||
// Get attachment icon based on attachTo type
|
||||
const getAttachmentIcon = (attachTo: AssetInput["attachTo"]) => {
|
||||
if (attachTo === "wall" || attachTo === "wall-side") {
|
||||
return "/icons/wall.png";
|
||||
}
|
||||
if (attachTo === "ceiling") {
|
||||
return "/icons/ceiling.png";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="-mx-2 -my-2 flex max-w-xl gap-2 overflow-x-auto p-2">
|
||||
{filteredItems.map((item, index) => {
|
||||
const isSelected = selectedItem?.src === item?.src;
|
||||
const attachmentIcon = getAttachmentIcon(item?.attachTo);
|
||||
return (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"relative aspect-square min-w-14 min-h-14 h-14 w-14 shrink-0 flex-col gap-px rounded-lg transition-all duration-200 ease-out hover:scale-105 hover:cursor-pointer",
|
||||
isSelected && "ring-2 ring-primary-foreground",
|
||||
)}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
type="button"
|
||||
>
|
||||
<Image
|
||||
alt={item.name}
|
||||
className="rounded-lg object-cover"
|
||||
fill
|
||||
src={item.thumbnail}
|
||||
/>
|
||||
{attachmentIcon && (
|
||||
<div className="absolute right-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
||||
<Image
|
||||
alt={
|
||||
item.attachTo === "ceiling"
|
||||
? "Ceiling attachment"
|
||||
: "Wall attachment"
|
||||
}
|
||||
className="h-4 w-4"
|
||||
height={16}
|
||||
src={attachmentIcon}
|
||||
width={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-xs" side="top">
|
||||
{item.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { BuildingNode, LevelNode, useScene } from "@pascal-app/core";
|
||||
import {
|
||||
AssetInput,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
useScene,
|
||||
} from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type Phase = "site" | "structure" | "furnish";
|
||||
|
||||
|
||||
export type Mode = "select" | "edit" | "delete" | "build";
|
||||
|
||||
// Structure mode tools (building elements)
|
||||
@@ -23,23 +27,20 @@ export type StructureTool =
|
||||
| "zone";
|
||||
|
||||
// Furnish mode tools (items and decoration)
|
||||
export type FurnishTool =
|
||||
| "item"
|
||||
export type FurnishTool = "item";
|
||||
|
||||
// Site mode tools
|
||||
export type SiteTool = "property-line";
|
||||
|
||||
|
||||
// Catalog categories for furnish mode items
|
||||
export type CatalogCategory =
|
||||
| 'furniture'
|
||||
| 'appliance'
|
||||
| 'bathroom'
|
||||
| 'kitchen'
|
||||
| 'outdoor'
|
||||
| 'window'
|
||||
| 'door'
|
||||
|
||||
| "furniture"
|
||||
| "appliance"
|
||||
| "bathroom"
|
||||
| "kitchen"
|
||||
| "outdoor"
|
||||
| "window"
|
||||
| "door";
|
||||
|
||||
// Combined tool type
|
||||
export type Tool = SiteTool | StructureTool | FurnishTool;
|
||||
@@ -53,6 +54,8 @@ type EditorState = {
|
||||
setTool: (tool: Tool | null) => void;
|
||||
catalogCategory: CatalogCategory | null;
|
||||
setCatalogCategory: (category: CatalogCategory | null) => void;
|
||||
selectedItem: AssetInput | null;
|
||||
setSelectedItem: (item: AssetInput) => void;
|
||||
};
|
||||
|
||||
const useEditor = create<EditorState>()((set, get) => ({
|
||||
@@ -98,7 +101,7 @@ const useEditor = create<EditorState>()((set, get) => ({
|
||||
// Maybe in furnish mode we force "solo" level view to see inside rooms
|
||||
viewer.setLevelMode("solo");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
mode: "select",
|
||||
setMode: (mode) => set({ mode }),
|
||||
@@ -106,6 +109,8 @@ const useEditor = create<EditorState>()((set, get) => ({
|
||||
setTool: (tool) => set({ tool }),
|
||||
catalogCategory: null,
|
||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
||||
selectedItem: null,
|
||||
setSelectedItem: (item) => set({ selectedItem: item }),
|
||||
}));
|
||||
|
||||
export default useEditor;
|
||||
|
||||
Reference in New Issue
Block a user