drafting tools
@@ -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() {
|
||||
<Stats />
|
||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||
<Passes />
|
||||
<WallEditor />
|
||||
<ToolManager />
|
||||
</Viewer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import useEditor, { Phase, Tool } from "@/store/use-editor";
|
||||
import { WallTool } from "./wall/wall-tool";
|
||||
|
||||
const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
||||
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 ? <Component /> : null;
|
||||
};
|
||||
@@ -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<Mesh>(null);
|
||||
const drawingLineRef = useRef<Line>(null!);
|
||||
@@ -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 (
|
||||
<TooltipProvider>
|
||||
<div
|
||||
@@ -18,6 +25,24 @@ export function ActionMenu({ className }: { className?: string }) {
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Control Mode Row - Always visible, centered */}
|
||||
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
||||
<PhaseSwitcher />
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{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 (
|
||||
<Tooltip key={`${tool.id}-${tool.catalogCategory ?? index}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={cn(
|
||||
'size-11 rounded-lg transition-all',
|
||||
isActive && 'bg-primary shadow-md shadow-primary/20',
|
||||
!isActive && 'hover:bg-white/10',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isActive) {
|
||||
setTool(null)
|
||||
setCatalogCategory(null)
|
||||
} else {
|
||||
setTool(tool.id)
|
||||
setCatalogCategory(tool.catalogCategory ?? null)
|
||||
}
|
||||
}}
|
||||
size="icon"
|
||||
variant={isActive ? 'default' : 'ghost'}
|
||||
>
|
||||
<NextImage
|
||||
alt={tool.label}
|
||||
className="size-full object-contain"
|
||||
height={28}
|
||||
src={tool.iconSrc}
|
||||
width={28}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{tool.label}
|
||||
{isActive && ' (Click to deselect)'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -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<EditorState>()((set, get) => ({
|
||||
@@ -94,6 +104,8 @@ const useEditor = create<EditorState>()((set, get) => ({
|
||||
setMode: (mode) => set({ mode }),
|
||||
tool: null,
|
||||
setTool: (tool) => set({ tool }),
|
||||
catalogCategory: null,
|
||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
||||
}));
|
||||
|
||||
export default useEditor;
|
||||
|
||||
@@ -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=="],
|
||||
|
||||