drafting tools

This commit is contained in:
wass08
2026-01-17 08:58:17 +09:00
parent 536c2e94cc
commit 59be752eb4
35 changed files with 166 additions and 12 deletions
+4 -3
View File
@@ -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>
)
}
+1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

+20 -8
View File
@@ -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;