diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx index acc718aa..929023d5 100644 --- a/apps/editor/components/editor.tsx +++ b/apps/editor/components/editor.tsx @@ -7,7 +7,7 @@ import { useScene, WallNode, } from "@pascal-app/core"; -import { useGridEvents, useNodeEvents, useViewer, Viewer } from "@pascal-app/viewer"; +import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer"; import { Stats } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber"; import { useEffect, useMemo, useRef } from "react"; @@ -27,7 +27,8 @@ import { } from "three/tsl"; import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu"; import { ActionMenu } from "./ui/action-menu"; -import { WallEditor } from "./editors/wall/wall-editor"; +import { WallTool } from "./tools/wall/wall-tool"; +import { ToolManager } from "./tools/tool-manager"; const selectedObjects: Object3D[] = []; @@ -42,7 +43,7 @@ export default function Editor() { - + ); diff --git a/apps/editor/components/tools/tool-manager.tsx b/apps/editor/components/tools/tool-manager.tsx new file mode 100644 index 00000000..7d0ed5c3 --- /dev/null +++ b/apps/editor/components/tools/tool-manager.tsx @@ -0,0 +1,25 @@ +import useEditor, { Phase, Tool } from "@/store/use-editor"; +import { WallTool } from "./wall/wall-tool"; + +const tools: Record>> = { + site: { + }, + structure: { + wall: WallTool, + }, + furnish: { + }, +}; + +export const ToolManager: React.FC = () => { + const phase = useEditor((state) => state.phase); + const mode = useEditor((state) => state.mode); + const tool = useEditor((state) => state.tool); + + if (mode !== "build" || tool === null) return null; + + + const Component = tools[phase]?.[tool]; + + return Component ? : null; +}; \ No newline at end of file diff --git a/apps/editor/components/editors/wall/wall-editor.tsx b/apps/editor/components/tools/wall/wall-tool.tsx similarity index 98% rename from apps/editor/components/editors/wall/wall-editor.tsx rename to apps/editor/components/tools/wall/wall-tool.tsx index c19b75e0..264ff50a 100644 --- a/apps/editor/components/editors/wall/wall-editor.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -3,7 +3,7 @@ import { useViewer } from "@pascal-app/viewer"; import { useEffect, useRef } from "react" import { Line, Mesh, Vector3 } from "three"; -export const WallEditor: React.FC = () => { +export const WallTool: React.FC = () => { const cursorRef = useRef(null); const drawingLineRef = useRef(null!); diff --git a/apps/editor/components/ui/action-menu/index.tsx b/apps/editor/components/ui/action-menu/index.tsx index 25084f5b..6000c073 100644 --- a/apps/editor/components/ui/action-menu/index.tsx +++ b/apps/editor/components/ui/action-menu/index.tsx @@ -5,9 +5,16 @@ import { cn } from "@/lib/utils"; import { ControlModes } from "./control-modes"; import { PhaseSwitcher } from "./phase-switcher"; +import { StructureTools } from "./structure-tools"; +import useEditor from "@/store/use-editor"; +import { AnimatePresence, motion } from "motion/react"; // import { ViewToggles } from "./view-toggles"; export function ActionMenu({ className }: { className?: string }) { + + const phase = useEditor((state) => state.phase); + const mode = useEditor((state) => state.mode); + return (
+ + {/* Structure Tools Row - Animated */} + + {phase === "structure" && mode === "build" && ( + +
+ +
+
+ )} +
{/* Control Mode Row - Always visible, centered */}
diff --git a/apps/editor/components/ui/action-menu/structure-tools.tsx b/apps/editor/components/ui/action-menu/structure-tools.tsx new file mode 100644 index 00000000..1c3a1c33 --- /dev/null +++ b/apps/editor/components/ui/action-menu/structure-tools.tsx @@ -0,0 +1,81 @@ +'use client' + +import NextImage from 'next/image' +import { Button } from '@/components/ui/primitives/button' +import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/primitives/tooltip' + +import { cn } from '@/lib/utils' +import useEditor, { CatalogCategory, StructureTool, Tool } from '@/store/use-editor' + +export type ToolConfig = { + id: StructureTool; iconSrc: string; label: string; catalogCategory?: CatalogCategory } + +export const tools: ToolConfig[] = [ + { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' }, + { id: 'room', iconSrc: '/icons/room.png', label: 'Room' }, + { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' }, + { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' }, + { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' }, + { id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' }, + { id: 'column', iconSrc: '/icons/column.png', label: 'Column' }, + { id: 'item', iconSrc: '/icons/door.png', label: 'Door', catalogCategory: 'door' }, + { id: 'item', iconSrc: '/icons/window.png', label: 'Window', catalogCategory: 'window' }, + { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' }, +] + +export function StructureTools() { + const activeTool = useEditor((state) => state.tool) + const catalogCategory = useEditor((state) => state.catalogCategory) + const setTool = useEditor((state) => state.setTool) + const setCatalogCategory = useEditor((state) => state.setCatalogCategory) + + return ( +
+ {tools.map((tool, index) => { + // For item tools with catalog category, check both tool and category match + const isActive = + activeTool === tool.id && + (tool.catalogCategory ? catalogCategory === tool.catalogCategory : true) + + return ( + + + + + +

+ {tool.label} + {isActive && ' (Click to deselect)'} +

+
+
+ ) + })} +
+ ) +} diff --git a/apps/editor/package.json b/apps/editor/package.json index df2b2dd1..ed65769b 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -22,6 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "motion": "^12.26.2", "next": "16.1.0", "postcss": "^8.5.6", "react": "^19.2.0", diff --git a/apps/editor/public/icons/appliance.png b/apps/editor/public/icons/appliance.png new file mode 100644 index 00000000..c2bb0bae Binary files /dev/null and b/apps/editor/public/icons/appliance.png differ diff --git a/apps/editor/public/icons/bathroom.png b/apps/editor/public/icons/bathroom.png new file mode 100644 index 00000000..fafec7cc Binary files /dev/null and b/apps/editor/public/icons/bathroom.png differ diff --git a/apps/editor/public/icons/building.png b/apps/editor/public/icons/building.png new file mode 100644 index 00000000..1d377680 Binary files /dev/null and b/apps/editor/public/icons/building.png differ diff --git a/apps/editor/public/icons/ceiling.png b/apps/editor/public/icons/ceiling.png new file mode 100644 index 00000000..9e04d639 Binary files /dev/null and b/apps/editor/public/icons/ceiling.png differ diff --git a/apps/editor/public/icons/collection.png b/apps/editor/public/icons/collection.png new file mode 100644 index 00000000..32b7ccd0 Binary files /dev/null and b/apps/editor/public/icons/collection.png differ diff --git a/apps/editor/public/icons/column.png b/apps/editor/public/icons/column.png new file mode 100644 index 00000000..8e44f6d8 Binary files /dev/null and b/apps/editor/public/icons/column.png differ diff --git a/apps/editor/public/icons/couch.png b/apps/editor/public/icons/couch.png new file mode 100644 index 00000000..6509e005 Binary files /dev/null and b/apps/editor/public/icons/couch.png differ diff --git a/apps/editor/public/icons/cube.png b/apps/editor/public/icons/cube.png new file mode 100644 index 00000000..c279d41b Binary files /dev/null and b/apps/editor/public/icons/cube.png differ diff --git a/apps/editor/public/icons/custom-room.png b/apps/editor/public/icons/custom-room.png new file mode 100644 index 00000000..14d40459 Binary files /dev/null and b/apps/editor/public/icons/custom-room.png differ diff --git a/apps/editor/public/icons/door.png b/apps/editor/public/icons/door.png new file mode 100644 index 00000000..8b045246 Binary files /dev/null and b/apps/editor/public/icons/door.png differ diff --git a/apps/editor/public/icons/environment.png b/apps/editor/public/icons/environment.png new file mode 100644 index 00000000..2bc71b11 Binary files /dev/null and b/apps/editor/public/icons/environment.png differ diff --git a/apps/editor/public/icons/floor.png b/apps/editor/public/icons/floor.png new file mode 100644 index 00000000..f2ce9f49 Binary files /dev/null and b/apps/editor/public/icons/floor.png differ diff --git a/apps/editor/public/icons/floorplan.png b/apps/editor/public/icons/floorplan.png new file mode 100644 index 00000000..456dce36 Binary files /dev/null and b/apps/editor/public/icons/floorplan.png differ diff --git a/apps/editor/public/icons/item.png b/apps/editor/public/icons/item.png new file mode 100644 index 00000000..ae3fdc25 Binary files /dev/null and b/apps/editor/public/icons/item.png differ diff --git a/apps/editor/public/icons/kitchen.png b/apps/editor/public/icons/kitchen.png new file mode 100644 index 00000000..1bffefe4 Binary files /dev/null and b/apps/editor/public/icons/kitchen.png differ diff --git a/apps/editor/public/icons/level.png b/apps/editor/public/icons/level.png new file mode 100644 index 00000000..4b931fd0 Binary files /dev/null and b/apps/editor/public/icons/level.png differ diff --git a/apps/editor/public/icons/mesh.png b/apps/editor/public/icons/mesh.png new file mode 100644 index 00000000..ecd7a21e Binary files /dev/null and b/apps/editor/public/icons/mesh.png differ diff --git a/apps/editor/public/icons/roof.png b/apps/editor/public/icons/roof.png new file mode 100644 index 00000000..253c1d76 Binary files /dev/null and b/apps/editor/public/icons/roof.png differ diff --git a/apps/editor/public/icons/room.png b/apps/editor/public/icons/room.png new file mode 100644 index 00000000..85a179b4 Binary files /dev/null and b/apps/editor/public/icons/room.png differ diff --git a/apps/editor/public/icons/site.png b/apps/editor/public/icons/site.png new file mode 100644 index 00000000..0d37688a Binary files /dev/null and b/apps/editor/public/icons/site.png differ diff --git a/apps/editor/public/icons/stairs.png b/apps/editor/public/icons/stairs.png new file mode 100644 index 00000000..6c9f1a65 Binary files /dev/null and b/apps/editor/public/icons/stairs.png differ diff --git a/apps/editor/public/icons/tree.png b/apps/editor/public/icons/tree.png new file mode 100644 index 00000000..ae3fdc25 Binary files /dev/null and b/apps/editor/public/icons/tree.png differ diff --git a/apps/editor/public/icons/wall.png b/apps/editor/public/icons/wall.png new file mode 100644 index 00000000..465e3e9d Binary files /dev/null and b/apps/editor/public/icons/wall.png differ diff --git a/apps/editor/public/icons/wallcut.png b/apps/editor/public/icons/wallcut.png new file mode 100644 index 00000000..bedcf614 Binary files /dev/null and b/apps/editor/public/icons/wallcut.png differ diff --git a/apps/editor/public/icons/walllow.png b/apps/editor/public/icons/walllow.png new file mode 100644 index 00000000..7ec71abd Binary files /dev/null and b/apps/editor/public/icons/walllow.png differ diff --git a/apps/editor/public/icons/window.png b/apps/editor/public/icons/window.png new file mode 100644 index 00000000..ec62d0a7 Binary files /dev/null and b/apps/editor/public/icons/window.png differ diff --git a/apps/editor/public/icons/zone.png b/apps/editor/public/icons/zone.png new file mode 100644 index 00000000..32b7ccd0 Binary files /dev/null and b/apps/editor/public/icons/zone.png differ diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx index ceb439f8..390b468b 100644 --- a/apps/editor/store/use-editor.tsx +++ b/apps/editor/store/use-editor.tsx @@ -5,6 +5,8 @@ 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) @@ -17,22 +19,28 @@ export type StructureTool = | "roof" | "column" | "stair" - | "door" - | "window" + | "item" | "zone"; // Furnish mode tools (items and decoration) export type FurnishTool = - | "furniture" - | "appliance" - | "kitchen" - | "bathroom" - | "outdoor" - | "painting"; + | "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' + + // Combined tool type export type Tool = SiteTool | StructureTool | FurnishTool; @@ -43,6 +51,8 @@ type EditorState = { setMode: (mode: Mode) => void; tool: Tool | null; setTool: (tool: Tool | null) => void; + catalogCategory: CatalogCategory | null; + setCatalogCategory: (category: CatalogCategory | null) => void; }; const useEditor = create()((set, get) => ({ @@ -94,6 +104,8 @@ const useEditor = create()((set, get) => ({ setMode: (mode) => set({ mode }), tool: null, setTool: (tool) => set({ tool }), + catalogCategory: null, + setCatalogCategory: (category) => set({ catalogCategory: category }), })); export default useEditor; diff --git a/bun.lock b/bun.lock index 4f8420b7..8f942691 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "motion": "^12.26.2", "next": "16.1.0", "postcss": "^8.5.6", "react": "^19.2.0", @@ -625,6 +626,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -817,6 +820,12 @@ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "motion": ["motion@12.26.2", "", { "dependencies": { "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw=="], + + "motion-dom": ["motion-dom@12.26.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="], + + "motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],