'use client' import { useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { AnimatePresence, motion } from 'motion/react' import { useEffect, useMemo } from 'react' import { MaterialPicker } from './../../../components/ui/controls/material-picker' import { TooltipProvider } from './../../../components/ui/primitives/tooltip' import { useIsMobile } from './../../../hooks/use-mobile' import { useReducedMotion } from './../../../hooks/use-reduced-motion' import { resolvePaintTargetFromSelection } from './../../../lib/material-paint' import { cn } from './../../../lib/utils' import useEditor from './../../../store/use-editor' import { CameraActions } from './camera-actions' import { ControlModes } from './control-modes' import { StructureTools } from './structure-tools' import { GridSnapControl, SecondaryToggles } from './view-toggles' // Mobile bottom offset matches the viewer's overlap behind the sheet's // rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits // just above that strip instead of inside it. const MOBILE_BOTTOM_OFFSET = 24 function PaintMaterialTray() { const activePaintMaterial = useEditor((state) => state.activePaintMaterial) const activePaintTarget = useEditor((state) => state.activePaintTarget) const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial) const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget) const selectedIds = useViewer((state) => state.selection.selectedIds) const nodes = useScene((state) => state.nodes) const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null useEffect(() => { const selectedPaintTarget = resolvePaintTargetFromSelection({ nodes, selectedId, }) if (selectedPaintTarget) { setActivePaintTarget(selectedPaintTarget) } }, [nodes, selectedId, setActivePaintTarget]) return (
{ setActivePaintMaterial({ material, sourceTarget: activePaintTarget }) }} onSelectMaterialPreset={(materialPreset) => { setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget }) }} selectedMaterialPreset={activePaintMaterial?.materialPreset} value={activePaintMaterial?.material} />
) } 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) const isMobile = useIsMobile() const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0) const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId)) const CONTEXTUAL_TABS = new Set(['ai', 'items', 'studio']) const isContextualPanelOnMobile = useEditor( (s) => isMobile && CONTEXTUAL_TABS.has(s.activeSidebarPanel), ) const reducedMotion = useReducedMotion() const showPaintTray = useMemo(() => mode === 'material-paint', [mode]) // On mobile, defer the bottom rail to the selection bar when something // is selected — the contextual actions take priority over mode controls. // Also hide on Chat / Items / Studio tabs; those are contextual workflows // (composing / picking furniture / generating renders) where the build // menu is irrelevant. if (hasSelectionOnMobile || hasReferenceOnMobile || isContextualPanelOnMobile) return null const transition = reducedMotion ? { duration: 0 } : { type: 'spring' as const, bounce: 0.2, duration: 0.4 } return ( {/* Structure Tools Row - Animated */} {phase === 'structure' && mode === 'build' && (
)}
{showPaintTray && ( )} {isMobile ? (
{/* Row 1: control modes only */}
{/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
) : (
)} ) }