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/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..25a2bf70 --- /dev/null +++ b/apps/editor/components/ui/panels/panel-manager.tsx @@ -0,0 +1,34 @@ +'use client' + +import { 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 node = nodes[selectedIds[0]!] + 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/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts index 8e8e75ad..ba129bbc 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -435,7 +435,7 @@ export class SpatialGridManager { const slabMap = this.slabsByLevel.get(levelId) if (!slabMap) return 0 - let maxElevation = 0 + let maxElevation = -Infinity for (const slab of slabMap.values()) { if (slab.polygon.length >= 3 && itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) { const elevation = slab.elevation ?? 0.05 @@ -444,7 +444,7 @@ export class SpatialGridManager { } } } - return maxElevation + return maxElevation === -Infinity ? 0 : maxElevation } /** diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index eb292331..213d9e8b 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -151,6 +151,7 @@ export function generateExtrudedWall( const wallStart: Point2D = { x: wallNode.start[0], y: wallNode.start[1] } const wallEnd: Point2D = { x: wallNode.end[0], y: wallNode.end[1] } + // Wall height is adjusted by slab elevation (positive reduces, negative increases) const height = (wallNode.height ?? 2.5) - slabElevation const thickness = wallNode.thickness ?? 0.1 const halfT = thickness / 2 @@ -244,7 +245,8 @@ export function generateExtrudedWall( // Rotate so extrusion direction (Z) becomes height direction (Y) geometry.rotateX(-Math.PI / 2) - if (slabElevation > 0) { + // Translate by slab elevation (works for both positive and negative values) + if (slabElevation !== 0) { geometry.translate(0, slabElevation, 0) } geometry.computeVertexNormals()