diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 49b90abd..e20d54bd 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -13,8 +13,7 @@ import { useKeyboard } from '@/hooks/use-keyboard' import { ZoneSystem } from '../systems/zone/zone-system' import { ToolManager } from '../tools/tool-manager' import { ActionMenu } from '../ui/action-menu' -import { ReferencePanel } from '../ui/panels/reference-panel' -import { RoofPanel } from '../ui/panels/roof-panel' +import { PanelManager } from '../ui/panels/panel-manager' import { SidebarProvider } from '../ui/primitives/sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar' import { CustomCameraControls } from './custom-camera-controls' @@ -31,8 +30,7 @@ export default function Editor() { return (
- - + diff --git a/apps/editor/components/tools/item/use-placement-coordinator.tsx b/apps/editor/components/tools/item/use-placement-coordinator.tsx index 34d02fae..f1c12000 100644 --- a/apps/editor/components/tools/item/use-placement-coordinator.tsx +++ b/apps/editor/components/tools/item/use-placement-coordinator.tsx @@ -503,12 +503,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea if (!asset.attachTo) { const levelId = useViewer.getState().selection.levelId if (levelId) { - mesh.position.y = spatialGridManager.getSlabElevationForItem( + const slabElevation = spatialGridManager.getSlabElevationForItem( levelId, [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z], asset.dimensions ?? DEFAULT_DIMENSIONS, draftNode.current.rotation, ) + mesh.position.y = slabElevation + cursorRef.current.position.y = slabElevation } } } diff --git a/apps/editor/components/tools/slab/slab-tool.tsx b/apps/editor/components/tools/slab/slab-tool.tsx index 5f915278..6440b302 100644 --- a/apps/editor/components/tools/slab/slab-tool.tsx +++ b/apps/editor/components/tools/slab/slab-tool.tsx @@ -46,12 +46,12 @@ const calculateSnapPoint = ( }; /** - * Creates a slab with the given polygon points + * Creates a slab with the given polygon points and returns its ID */ const commitSlabDrawing = ( levelId: LevelNode["id"], points: Array<[number, number]> -) => { +): string => { const { createNode, nodes } = useScene.getState(); // Count existing slabs for naming @@ -64,6 +64,7 @@ const commitSlabDrawing = ( }); createNode(slab, levelId); + return slab.id; }; type PreviewState = { @@ -87,6 +88,7 @@ export const SlabTool: React.FC = () => { const pointsRef = useRef>([]); const levelYRef = useRef(0); // Track current level Y position const currentLevelId = useViewer((state) => state.selection.levelId); + const setSelection = useViewer((state) => state.setSelection); const setTool = useEditor((state) => state.setTool); // Preview state for reactive rendering (for shape and point markers) @@ -213,17 +215,15 @@ export const SlabTool: React.FC = () => { Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 && Math.abs(clickPoint[1] - firstPoint[1]) < 0.25 ) { - // Create the slab - commitSlabDrawing(currentLevelId, pointsRef.current); + // Create the slab and select it + const slabId = commitSlabDrawing(currentLevelId, pointsRef.current); + setSelection({ selectedIds: [slabId] }); // Reset state pointsRef.current = []; setPreview({ points: [], cursorPoint: null, levelY: 0 }); mainLineRef.current.visible = false; closingLineRef.current.visible = false; - - // Deactivate tool - setTool(null); } else { // Add point to polygon pointsRef.current = [...pointsRef.current, clickPoint]; @@ -236,16 +236,15 @@ export const SlabTool: React.FC = () => { // Need at least 3 points to form a polygon if (pointsRef.current.length >= 3) { - commitSlabDrawing(currentLevelId, pointsRef.current); + // Create the slab and select it + const slabId = commitSlabDrawing(currentLevelId, pointsRef.current); + setSelection({ selectedIds: [slabId] }); // Reset state pointsRef.current = []; setPreview({ points: [], cursorPoint: null, levelY: 0 }); mainLineRef.current.visible = false; closingLineRef.current.visible = false; - - // Deactivate tool - setTool(null); } }; @@ -262,7 +261,7 @@ export const SlabTool: React.FC = () => { // Reset state on unmount pointsRef.current = []; }; - }, [currentLevelId, setTool]); + }, [currentLevelId, setSelection]); const { points, cursorPoint, levelY } = preview; diff --git a/apps/editor/components/ui/panels/panel-manager.tsx b/apps/editor/components/ui/panels/panel-manager.tsx new file mode 100644 index 00000000..92d26f68 --- /dev/null +++ b/apps/editor/components/ui/panels/panel-manager.tsx @@ -0,0 +1,35 @@ +'use client' + +import { AnyNodeId, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import useEditor from '@/store/use-editor' +import { ReferencePanel } from './reference-panel' +import { RoofPanel } from './roof-panel' +import { SlabPanel } from './slab-panel' + +export function PanelManager() { + const selectedIds = useViewer((s) => s.selection.selectedIds) + const selectedReferenceId = useEditor((s) => s.selectedReferenceId) + const nodes = useScene((s) => s.nodes) + + // Show reference panel if a reference is selected + if (selectedReferenceId) { + return + } + + // Show appropriate panel based on selected node type + if (selectedIds.length === 1) { + const selectedNode = selectedIds[0] + const node = nodes[selectedNode as AnyNodeId] + if (node) { + switch (node.type) { + case 'roof': + return + case 'slab': + return + } + } + } + + return null +} diff --git a/apps/editor/components/ui/panels/slab-panel.tsx b/apps/editor/components/ui/panels/slab-panel.tsx new file mode 100644 index 00000000..cd0c1f7e --- /dev/null +++ b/apps/editor/components/ui/panels/slab-panel.tsx @@ -0,0 +1,148 @@ +'use client' + +import { type AnyNode, type SlabNode, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { X } from 'lucide-react' +import Image from 'next/image' +import { useCallback } from 'react' + +export function SlabPanel() { + const selectedIds = useViewer((s) => s.selection.selectedIds) + const setSelection = useViewer((s) => s.setSelection) + const nodes = useScene((s) => s.nodes) + const updateNode = useScene((s) => s.updateNode) + + // Get the first selected node if it's a slab + const selectedId = selectedIds[0] + const node = selectedId + ? (nodes[selectedId as AnyNode['id']] as SlabNode | undefined) + : undefined + + const handleUpdate = useCallback( + (updates: Partial) => { + if (!selectedId) return + updateNode(selectedId as AnyNode['id'], updates) + }, + [selectedId, updateNode], + ) + + const handleClose = useCallback(() => { + setSelection({ selectedIds: [] }) + }, [setSelection]) + + // Only show if exactly one slab is selected + if (!node || node.type !== 'slab' || selectedIds.length !== 1) return null + + // Calculate approximate area from polygon + const calculateArea = (polygon: Array<[number, number]>): number => { + if (polygon.length < 3) return 0 + let area = 0 + const n = polygon.length + for (let i = 0; i < n; i++) { + const j = (i + 1) % n + area += polygon[i]![0] * polygon[j]![1] + area -= polygon[j]![0] * polygon[i]![1] + } + return Math.abs(area) / 2 + } + + const area = calculateArea(node.polygon) + + return ( +
+ {/* Header */} +
+
+ +

+ {node.name || `Slab (${area.toFixed(1)}m²)`} +

+
+ +
+ + {/* Content */} +
+
+ {/* Elevation */} +
+ +
+ { + const value = Number.parseFloat(e.target.value) + if (!Number.isNaN(value)) { + handleUpdate({ elevation: value }) + } + }} + step="0.05" + type="number" + value={Math.round(node.elevation * 1000) / 1000} + /> + m +
+

+ Height offset from the level base (positive = raised, negative = sunken) +

+
+ + {/* Quick preset buttons */} +
+ +
+ + + + +
+
+ + {/* Area info */} +
+ +
+ {area.toFixed(2)} m² +
+
+
+
+
+ ) +} diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx index ea87fb74..017d4dba 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx @@ -59,6 +59,7 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) { onMouseLeave={handleMouseLeave} isSelected={isSelected} isHovered={isHovered} + isVisible={node.visible !== false} actions={} > {node.children.map((childId) => ( diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx index b53da349..a6924faf 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -1,5 +1,6 @@ import { type BuildingNode, + emitter, LevelNode, useScene, type ZoneNode, @@ -7,6 +8,7 @@ import { import { useViewer } from "@pascal-app/viewer"; import { Building2, + Camera, ChevronDown, Layers, MoreHorizontal, @@ -155,11 +157,13 @@ function BuildingSelector() { function LevelsSection() { const nodes = useScene((state) => state.nodes); const createNode = useScene((state) => state.createNode); + const updateNode = useScene((state) => state.updateNode); const selectedBuildingId = useViewer((state) => state.selection.buildingId); const selectedLevelId = useViewer((state) => state.selection.levelId); const setSelection = useViewer((state) => state.setSelection); const [referencesLevelId, setReferencesLevelId] = useState(null); + const [cameraPopoverOpen, setCameraPopoverOpen] = useState(null); const building = selectedBuildingId ? (nodes[selectedBuildingId] as BuildingNode) @@ -215,6 +219,72 @@ function LevelsSection() { {level.name || `Level ${level.level}`} + {/* Camera snapshot button */} + setCameraPopoverOpen(open ? level.id : null)}> + + + + e.stopPropagation()} + > +
+ {level.camera && ( + + )} + + {level.camera && ( + + )} +
+
+
+ + e.stopPropagation()} + > +
+ {zone.camera && ( + + )} + + {zone.camera && ( + + )} +
+
+
+ + e.stopPropagation()} + > +
+ {zone.camera && ( + + )} + + {zone.camera && ( + + )} +
+
+