diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index e63890da..56627adc 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -1,9 +1,10 @@ 'use client' -import { useEffect } from 'react' import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core' -import { Viewer, useViewer } from '@pascal-app/viewer' +import { useViewer, Viewer } from '@pascal-app/viewer' +import { useEffect } from 'react' import { useProjectScene } from '@/features/community/lib/models/hooks' +import { useProjectStore } from '@/features/community/lib/projects/store' import { useKeyboard } from '@/hooks/use-keyboard' import { initSFXBus } from '@/lib/sfx-bus' import useEditor from '@/store/use-editor' @@ -16,6 +17,7 @@ import { ActionMenu } from '../ui/action-menu' import { HelperManager } from '../ui/helpers/helper-manager' import { PanelManager } from '../ui/panels/panel-manager' import { SidebarProvider } from '../ui/primitives/sidebar' +import { SceneLoader } from '../ui/scene-loader' import { AppSidebar } from '../ui/sidebar/app-sidebar' import { CustomCameraControls } from './custom-camera-controls' import { ExportManager } from './export-manager' @@ -23,8 +25,6 @@ import { FloatingActionMenu } from './floating-action-menu' import { Grid } from './grid' import { SelectionManager } from './selection-manager' import { ThumbnailGenerator } from './thumbnail-generator' -import { useProjectStore } from '@/features/community/lib/projects/store' -import { SceneLoader } from '../ui/scene-loader' // Load default scene initially (will be replaced when project loads) useScene.getState().loadScene() @@ -35,7 +35,7 @@ initSpaceDetectionSync(useScene, useEditor) const sceneNodes = useScene.getState().nodes as Record const sceneRootIds = useScene.getState().rootNodeIds const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null -const resolve = (child: any) => typeof child === 'string' ? sceneNodes[child] : child +const resolve = (child: any) => (typeof child === 'string' ? sceneNodes[child] : child) const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building') const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level') @@ -48,7 +48,7 @@ if (firstBuilding && firstLevel) { }) useEditor.getState().setPhase('structure') useEditor.getState().setStructureLayer('elements') - + // Auto-select the wall tool if the level is empty if (!firstLevel.children || firstLevel.children.length === 0) { useEditor.getState().setMode('build') diff --git a/apps/editor/components/feedback-dialog.tsx b/apps/editor/components/feedback-dialog.tsx index d4386f1d..2a63dea0 100644 --- a/apps/editor/components/feedback-dialog.tsx +++ b/apps/editor/components/feedback-dialog.tsx @@ -1,13 +1,10 @@ 'use client' -import { ImageIcon, MessageSquare, X } from 'lucide-react' -import { useCallback, useRef, useState } from 'react' -import { useParams } from 'next/navigation' import { useScene } from '@pascal-app/core' -import { - createImageUploadUrls, - submitFeedback, -} from '@/features/community/lib/feedback/actions' +import { ImageIcon, MessageSquare, X } from 'lucide-react' +import { useParams } from 'next/navigation' +import { useCallback, useRef, useState } from 'react' +import { Button } from '@/components/ui/primitives/button' import { Dialog, DialogContent, @@ -15,7 +12,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/primitives/dialog' -import { Button } from '@/components/ui/primitives/button' +import { createImageUploadUrls, submitFeedback } from '@/features/community/lib/feedback/actions' const MAX_IMAGES = 5 const MAX_IMAGE_SIZE = 5 * 1024 * 1024 @@ -49,25 +46,24 @@ export function FeedbackDialog({ projectId: projectIdProp }: { projectId?: strin const handleClose = () => { if (isSubmitting) return setOpen(false) - images.forEach((img) => URL.revokeObjectURL(img.url)) + images.forEach((img) => { + URL.revokeObjectURL(img.url) + }) } - const addFiles = useCallback( - (files: FileList | File[]) => { - const incoming = Array.from(files).filter( - (f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE, - ) - setImages((prev) => { - const remaining = MAX_IMAGES - prev.length - const added = incoming.slice(0, remaining).map((file) => ({ - file, - url: URL.createObjectURL(file), - })) - return [...prev, ...added] - }) - }, - [], - ) + const addFiles = useCallback((files: FileList | File[]) => { + const incoming = Array.from(files).filter( + (f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE, + ) + setImages((prev) => { + const remaining = MAX_IMAGES - prev.length + const added = incoming.slice(0, remaining).map((file) => ({ + file, + url: URL.createObjectURL(file), + })) + return [...prev, ...added] + }) + }, []) const removeImage = (index: number) => { setImages((prev) => { @@ -190,7 +186,7 @@ export function FeedbackDialog({ projectId: projectIdProp }: { projectId?: strin <>
- -

+

{currentTrack.title}

diff --git a/apps/editor/components/ui/action-menu/action-button.tsx b/apps/editor/components/ui/action-menu/action-button.tsx new file mode 100644 index 00000000..d9612d55 --- /dev/null +++ b/apps/editor/components/ui/action-menu/action-button.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { Button } from "@/components/ui/primitives/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/primitives/tooltip"; +import { cn } from "@/lib/utils"; + +interface ActionButtonProps extends React.ComponentProps { + label: string; + shortcut?: string; + isActive?: boolean; + tooltipContent?: React.ReactNode; +} + +export const ActionButton = React.forwardRef( + ( + { className, children, label, shortcut, isActive, tooltipContent, ...props }, + ref + ) => { + return ( + + + + + + {tooltipContent || ( +

+ {label} {shortcut && `(${shortcut})`} +

+ )} +
+
+ ); + } +); +ActionButton.displayName = "ActionButton"; diff --git a/apps/editor/components/ui/action-menu/camera-actions.tsx b/apps/editor/components/ui/action-menu/camera-actions.tsx index 63309368..c25c3259 100644 --- a/apps/editor/components/ui/action-menu/camera-actions.tsx +++ b/apps/editor/components/ui/action-menu/camera-actions.tsx @@ -2,12 +2,7 @@ import { emitter } from '@pascal-app/core' import Image from 'next/image' -import { Button } from '@/components/ui/primitives/button' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/primitives/tooltip' +import { ActionButton } from "./action-button"; export function CameraActions() { const goToTopView = () => { @@ -25,73 +20,55 @@ export function CameraActions() { return (
{/* Orbit CCW */} - - - - - -

Orbit Left

-
-
+ + Orbit Left + {/* Orbit CW */} - - - - - -

Orbit Right

-
-
+ + Orbit Right + {/* Top View */} - - - - - -

Top View

-
-
+ + Top View +
) } diff --git a/apps/editor/components/ui/action-menu/control-modes.tsx b/apps/editor/components/ui/action-menu/control-modes.tsx index 15b97c62..674985c4 100644 --- a/apps/editor/components/ui/action-menu/control-modes.tsx +++ b/apps/editor/components/ui/action-menu/control-modes.tsx @@ -1,12 +1,7 @@ "use client"; import Image from "next/image"; -import { Button } from "@/components/ui/primitives/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/primitives/tooltip"; +import { ActionButton } from "./action-button"; import { Pencil, Trash2, type LucideIcon } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -103,44 +98,37 @@ export function ControlModes() { const isImageMode = Boolean(m.imageSrc); return ( - - - - - -

- {m.label} ({m.shortcut}) -

-
-
+ height={28} + src={m.imageSrc} + width={28} + /> + ) : ( + Icon && + )} + ); })} diff --git a/apps/editor/components/ui/action-menu/furnish-tools.tsx b/apps/editor/components/ui/action-menu/furnish-tools.tsx index 34ae9d5a..b6c7c774 100644 --- a/apps/editor/components/ui/action-menu/furnish-tools.tsx +++ b/apps/editor/components/ui/action-menu/furnish-tools.tsx @@ -1,12 +1,7 @@ "use client"; import NextImage from "next/image"; -import { Button } from "@/components/ui/primitives/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/primitives/tooltip"; +import { ActionButton } from "./action-button"; import { cn } from "@/lib/utils"; import useEditor, { CatalogCategory } from "@/store/use-editor"; @@ -76,40 +71,33 @@ export function FurnishTools() { catalogCategory === tool.catalogCategory; return ( - - - - - -

- {tool.label} -

-
-
+ { + if (!isActive) { + setCatalogCategory(tool.catalogCategory); + setActiveTool("item"); + if (mode !== "build") { + setMode("build"); + } + } + }} + size="icon" + variant="ghost" + > + + ); })} diff --git a/apps/editor/components/ui/action-menu/structure-tools.tsx b/apps/editor/components/ui/action-menu/structure-tools.tsx index 9df6beeb..df5d03a7 100644 --- a/apps/editor/components/ui/action-menu/structure-tools.tsx +++ b/apps/editor/components/ui/action-menu/structure-tools.tsx @@ -1,8 +1,7 @@ 'use client' import NextImage from 'next/image' -import { Button } from '@/components/ui/primitives/button' -import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/primitives/tooltip' +import { ActionButton } from "./action-button"; import { cn } from '@/lib/utils' import useEditor, { CatalogCategory, StructureTool, Tool } from '@/store/use-editor' @@ -53,42 +52,35 @@ export function StructureTools() { const isContextual = contextualTools.includes(tool.id) return ( - - - - - -

- {tool.label} -

-
-
+ { + if (!isActive) { + setTool(tool.id) + setCatalogCategory(tool.catalogCategory ?? null) + + // Automatically switch to build mode if we select a tool + if (useEditor.getState().mode !== 'build') { + useEditor.getState().setMode('build') + } + } + }} + size="icon" + variant="ghost" + > + + ) })} diff --git a/apps/editor/components/ui/action-menu/view-toggles.tsx b/apps/editor/components/ui/action-menu/view-toggles.tsx index 330385bd..52bd85dc 100644 --- a/apps/editor/components/ui/action-menu/view-toggles.tsx +++ b/apps/editor/components/ui/action-menu/view-toggles.tsx @@ -2,12 +2,7 @@ import { useViewer } from '@pascal-app/viewer' import { Box, Camera, Diamond, Image, Layers, Layers2 } from 'lucide-react' -import { Button } from '@/components/ui/primitives/button' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/primitives/tooltip' +import { ActionButton } from "./action-button"; import { cn } from '@/lib/utils' const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = { @@ -81,119 +76,87 @@ export function ViewToggles() { return (
{/* Camera Mode */} - - - - - -

Camera: {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}

-
-
+ + + {/* Level Mode */} - - - - - -

Levels: {levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode as keyof typeof levelModeLabels]}

-
-
+ + {levelMode === 'solo' && } + {levelMode === 'exploded' && } + {(levelMode === 'stacked' || levelMode === 'manual') && } + {/* Wall Mode */} - - - - - -

Walls: {wallModeConfig[wallMode].label}

-
-
+ + {(() => { + const Icon = wallModeConfig[wallMode].icon + return + })()} + {/* Show Scans */} - - - - - -

Scans: {showScans ? 'Visible' : 'Hidden'}

-
-
+ setShowScans(!showScans)} + size="icon" + variant="ghost" + > + Scans + {/* Show Guides */} - - - - - -

Guides: {showGuides ? 'Visible' : 'Hidden'}

-
-
+ setShowGuides(!showGuides)} + size="icon" + variant="ghost" + > + Guides +
) } diff --git a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx index 70b620ec..71113dcb 100644 --- a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx @@ -20,6 +20,7 @@ import { import { Switch } from "@/components/ui/primitives/switch"; import useEditor from "@/store/use-editor"; import { AudioSettingsDialog } from "./audio-settings-dialog"; +import { KeyboardShortcutsDialog } from "./keyboard-shortcuts-dialog"; import { useProjectStore } from "@/features/community/lib/projects/store"; import { updateProjectVisibility } from "@/features/community/lib/projects/actions"; @@ -395,6 +396,14 @@ export function SettingsPanel() { + {/* Keyboard Section */} +
+ + +
+ {/* Scene Graph */}
+
+ + S + +
); diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx index 63160dcd..f525f808 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx @@ -1,4 +1,5 @@ -import { type AnyNode, emitter, useScene } from "@pascal-app/core"; +import { type AnyNode, type AnyNodeId, emitter, useScene } from "@pascal-app/core"; +import { useViewer } from "@pascal-app/viewer"; import { Camera, Eye, EyeOff, Trash2 } from "lucide-react"; import { useState } from "react"; import { @@ -14,12 +15,24 @@ interface TreeNodeActionsProps { export function TreeNodeActions({ node }: TreeNodeActionsProps) { const [open, setOpen] = useState(false); const updateNode = useScene((state) => state.updateNode); + const updateNodes = useScene((state) => state.updateNodes); + const selectedIds = useViewer((state) => state.selection.selectedIds); const hasCamera = !!node.camera; const isVisible = node.visible !== false; const toggleVisibility = (e: React.MouseEvent) => { e.stopPropagation(); - updateNode(node.id, { visible: !isVisible }); + const newVisibility = !isVisible; + if (selectedIds && selectedIds.includes(node.id)) { + updateNodes( + selectedIds.map((id) => ({ + id: id as AnyNodeId, + data: { visible: newVisibility }, + })) + ); + } else { + updateNode(node.id, { visible: newVisibility }); + } }; const handleCaptureCamera = (e: React.MouseEvent) => { diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx index 51dde090..4fa80cf7 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx @@ -212,12 +212,18 @@ export const TreeNodeWrapper = forwardRef( )}> {icon} -
+
{label}
{actions && ( -
+
{actions}
)} diff --git a/apps/editor/hooks/use-keyboard.ts b/apps/editor/hooks/use-keyboard.ts index 64da7074..c0985007 100644 --- a/apps/editor/hooks/use-keyboard.ts +++ b/apps/editor/hooks/use-keyboard.ts @@ -37,6 +37,17 @@ export const useKeyboard = () => { e.preventDefault() useEditor.getState().setPhase('furnish') useEditor.getState().setMode('select') + } else if (e.key === 's' && !e.metaKey && !e.ctrlKey) { + e.preventDefault() + useEditor.getState().setPhase('structure') + useEditor.getState().setStructureLayer('elements') + } else if (e.key === 'f' && !e.metaKey && !e.ctrlKey) { + e.preventDefault() + useEditor.getState().setPhase('furnish') + } else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) { + e.preventDefault() + useEditor.getState().setPhase('structure') + useEditor.getState().setStructureLayer('zones') } if (e.key === 'v' && !e.metaKey && !e.ctrlKey) { e.preventDefault() diff --git a/apps/editor/public/icons/orbit.png b/apps/editor/public/icons/orbit.png new file mode 100644 index 00000000..d47d9d8c Binary files /dev/null and b/apps/editor/public/icons/orbit.png differ diff --git a/apps/editor/public/icons/pan.png b/apps/editor/public/icons/pan.png new file mode 100644 index 00000000..8cf276a9 Binary files /dev/null and b/apps/editor/public/icons/pan.png differ