Esc
Cancel
diff --git a/apps/editor/components/ui/item-catalog/catalog-items.tsx b/apps/editor/components/ui/item-catalog/catalog-items.tsx
index b775301d..876936cf 100644
--- a/apps/editor/components/ui/item-catalog/catalog-items.tsx
+++ b/apps/editor/components/ui/item-catalog/catalog-items.tsx
@@ -435,6 +435,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, 0],
rotation: [0, 0, 0],
dimensions: [2, 0.8, 1],
+ surface: {
+ height: 0.75
+ }
},
{
@@ -448,6 +451,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, 0],
rotation: [0, 0, 0],
dimensions: [2, 1.1, 1],
+ surface: {
+ height: 1.1
+ }
},
{
@@ -1161,7 +1167,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
scale: [1, 1, 1],
offset: [0, 0.21, 0],
rotation: [0, 0, 0],
- dimensions: [2, 0.4, 0.5],
+ dimensions: [2, 0.4, 0.5],surface: {
+ height: 0.36
+ }
},
{
@@ -1176,6 +1184,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
rotation: [0, 0, 0],
dimensions: [1, 0.5, 0.7],
attachTo: "wall-side",
+ surface: {
+ height: 0.12
+ }
},
{
@@ -1255,6 +1266,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, 0],
rotation: [0, 0, 0],
dimensions: [1.5, 0.8, 1],
+ surface: {
+ height: 0.8
+ }
},
{
@@ -1385,6 +1399,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, -0.01],
rotation: [0, 0, 0],
dimensions: [0.5, 0.5, 0.5],
+ surface: {
+ height: 0.5
+ }
},
{
@@ -1398,6 +1415,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, 0],
rotation: [0, 0, 0],
dimensions: [2, 0.4, 1.5],
+ surface: {
+ height: 0.3
+ }
},
{
@@ -1411,6 +1431,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, 0],
rotation: [0, 0, 0],
dimensions: [2, 0.8, 1],
+ surface: {
+ height: 0.75
+ }
},
{
@@ -1424,5 +1447,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
offset: [0, 0, -0.01],
rotation: [0, 0, 0],
dimensions: [2.5, 0.8, 1],
+ surface: {
+ height: 0.8
+ }
},
];
diff --git a/apps/editor/components/ui/panels/panel-manager.tsx b/apps/editor/components/ui/panels/panel-manager.tsx
index afef33ad..d5fa0b56 100644
--- a/apps/editor/components/ui/panels/panel-manager.tsx
+++ b/apps/editor/components/ui/panels/panel-manager.tsx
@@ -8,6 +8,8 @@ import { ItemPanel } from './item-panel'
import { ReferencePanel } from './reference-panel'
import { RoofPanel } from './roof-panel'
import { SlabPanel } from './slab-panel'
+import { WallPanel } from './wall-panel'
+import { WindowPanel } from './window-panel'
export function PanelManager() {
const selectedIds = useViewer((s) => s.selection.selectedIds)
@@ -33,6 +35,10 @@ export function PanelManager() {
return
case 'ceiling':
return
+ case 'wall':
+ return
+ case 'window':
+ return
}
}
}
diff --git a/apps/editor/components/ui/panels/wall-panel.tsx b/apps/editor/components/ui/panels/wall-panel.tsx
new file mode 100644
index 00000000..c51b937f
--- /dev/null
+++ b/apps/editor/components/ui/panels/wall-panel.tsx
@@ -0,0 +1,108 @@
+'use client'
+
+import { type AnyNode, type AnyNodeId, type WallNode, 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'
+import { NumberInput } from '@/components/ui/primitives/number-input'
+
+export function WallPanel() {
+ 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)
+
+ const selectedId = selectedIds[0]
+ const node = selectedId
+ ? (nodes[selectedId as AnyNode['id']] as WallNode | undefined)
+ : undefined
+
+ const handleUpdate = useCallback(
+ (updates: Partial
) => {
+ if (!selectedId) return
+ updateNode(selectedId as AnyNode['id'], updates)
+ useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
+ },
+ [selectedId, updateNode],
+ )
+
+ const handleClose = useCallback(() => {
+ setSelection({ selectedIds: [] })
+ }, [setSelection])
+
+ if (!node || node.type !== 'wall' || selectedIds.length !== 1) return null
+
+ const dx = node.end[0] - node.start[0]
+ const dz = node.end[1] - node.start[1]
+ const length = Math.sqrt(dx * dx + dz * dz)
+
+ const height = node.height ?? 2.5
+ const thickness = node.thickness ?? 0.1
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ {node.name || `Wall (${length.toFixed(2)}m)`}
+
+
+
+
+
+ {/* Content */}
+
+
+ {/* Dimensions */}
+
+
+
+ handleUpdate({ height: Math.max(0.1, v) })}
+ min={0.1}
+ precision={2}
+ step={0.1}
+ className="flex-1"
+ />
+ m
+
+
+ handleUpdate({ thickness: Math.max(0.05, v) })}
+ min={0.05}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+
+ {/* Info */}
+
+
+
+ Length: {length.toFixed(2)} m
+
+
+
+
+ )
+}
diff --git a/apps/editor/components/ui/panels/window-panel.tsx b/apps/editor/components/ui/panels/window-panel.tsx
new file mode 100644
index 00000000..da51ee5d
--- /dev/null
+++ b/apps/editor/components/ui/panels/window-panel.tsx
@@ -0,0 +1,339 @@
+'use client'
+
+import { type AnyNode, type AnyNodeId, type WindowNode, useScene } from '@pascal-app/core'
+import { useViewer } from '@pascal-app/viewer'
+import { FlipHorizontal2, X } from 'lucide-react'
+import Image from 'next/image'
+import { useCallback } from 'react'
+import { NumberInput } from '@/components/ui/primitives/number-input'
+import { Switch } from '@/components/ui/primitives/switch'
+
+export function WindowPanel() {
+ 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)
+
+ const selectedId = selectedIds[0]
+ const node = selectedId
+ ? (nodes[selectedId as AnyNode['id']] as WindowNode | undefined)
+ : undefined
+
+ const handleUpdate = useCallback(
+ (updates: Partial) => {
+ if (!selectedId) return
+ updateNode(selectedId as AnyNode['id'], updates)
+ useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
+ },
+ [selectedId, updateNode],
+ )
+
+ const handleClose = useCallback(() => {
+ setSelection({ selectedIds: [] })
+ }, [setSelection])
+
+ const handleFlip = useCallback(() => {
+ if (!node) return
+ handleUpdate({
+ side: node.side === 'front' ? 'back' : 'front',
+ rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
+ })
+ }, [node, handleUpdate])
+
+ if (!node || node.type !== 'window' || selectedIds.length !== 1) return null
+
+ const numCols = node.columnRatios.length
+ const numRows = node.rowRatios.length
+
+ // Normalized ratios (always sum to 1 for display)
+ const colSum = node.columnRatios.reduce((a, b) => a + b, 0)
+ const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
+ const normCols = node.columnRatios.map(r => r / colSum)
+ const normRows = node.rowRatios.map(r => r / rowSum)
+
+ const setColumnRatio = (index: number, newVal: number) => {
+ const clamped = Math.max(0.05, Math.min(0.95, newVal))
+ const neighborIdx = index < numCols - 1 ? index + 1 : index - 1
+ const delta = clamped - normCols[index]!
+ const neighborVal = Math.max(0.05, normCols[neighborIdx]! - delta)
+ const newRatios = normCols.map((v, i) => {
+ if (i === index) return clamped
+ if (i === neighborIdx) return neighborVal
+ return v
+ })
+ handleUpdate({ columnRatios: newRatios })
+ }
+
+ const setRowRatio = (index: number, newVal: number) => {
+ const clamped = Math.max(0.05, Math.min(0.95, newVal))
+ const neighborIdx = index < numRows - 1 ? index + 1 : index - 1
+ const delta = clamped - normRows[index]!
+ const neighborVal = Math.max(0.05, normRows[neighborIdx]! - delta)
+ const newRatios = normRows.map((v, i) => {
+ if (i === index) return clamped
+ if (i === neighborIdx) return neighborVal
+ return v
+ })
+ handleUpdate({ rowRatios: newRatios })
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ {node.name || `Window (${node.width}×${node.height}m)`}
+
+
+
+
+
+ {/* Content */}
+
+
+ {/* Position */}
+
+
+
+ handleUpdate({ position: [v, node.position[1], node.position[2]] })}
+ precision={2}
+ />
+ handleUpdate({ position: [node.position[0], v, node.position[2]] })}
+ precision={2}
+ />
+
+
+
+
+ {/* Dimensions */}
+
+
+
+
+ handleUpdate({ width: v })}
+ min={0.2}
+ precision={2}
+ className="flex-1"
+ />
+ m
+
+
+ handleUpdate({ height: v })}
+ min={0.2}
+ precision={2}
+ className="flex-1"
+ />
+ m
+
+
+
+
+ {/* Frame */}
+
+
+
+
+ handleUpdate({ frameThickness: v })}
+ min={0.01}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+ handleUpdate({ frameDepth: v })}
+ min={0.01}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+
+
+ {/* Grid */}
+
+
+
+ {
+ const n = Math.max(1, Math.min(8, Math.round(v)))
+ handleUpdate({ columnRatios: Array(n).fill(1 / n) })
+ }}
+ min={1}
+ max={8}
+ precision={0}
+ step={1}
+ />
+ {
+ const n = Math.max(1, Math.min(8, Math.round(v)))
+ handleUpdate({ rowRatios: Array(n).fill(1 / n) })
+ }}
+ min={1}
+ max={8}
+ precision={0}
+ step={1}
+ />
+
+
+ {/* Column ratios */}
+ {numCols > 1 && (
+
+
Column widths
+ {normCols.map((ratio, i) => (
+
+ setColumnRatio(i, v / 100)}
+ min={5}
+ max={95}
+ precision={1}
+ step={1}
+ className="flex-1"
+ />
+ %
+
+ ))}
+
+ handleUpdate({ columnDividerThickness: v })}
+ min={0.005}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+ )}
+
+ {/* Row ratios */}
+ {numRows > 1 && (
+
+
Row heights
+ {normRows.map((ratio, i) => (
+
+ setRowRatio(i, v / 100)}
+ min={5}
+ max={95}
+ precision={1}
+ step={1}
+ className="flex-1"
+ />
+ %
+
+ ))}
+
+ handleUpdate({ rowDividerThickness: v })}
+ min={0.005}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+ )}
+
+
+ {/* Sill */}
+
+
+
+ handleUpdate({ sill: checked })}
+ />
+
+ {node.sill && (
+
+
+ handleUpdate({ sillDepth: v })}
+ min={0.01}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+ handleUpdate({ sillThickness: v })}
+ min={0.005}
+ precision={3}
+ step={0.01}
+ className="flex-1"
+ />
+ m
+
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/editor/components/ui/primitives/number-input.tsx b/apps/editor/components/ui/primitives/number-input.tsx
index eb6659ef..acbdd39e 100644
--- a/apps/editor/components/ui/primitives/number-input.tsx
+++ b/apps/editor/components/ui/primitives/number-input.tsx
@@ -10,6 +10,7 @@ interface NumberInputProps {
min?: number
max?: number
precision?: number
+ step?: number
className?: string
}
@@ -20,6 +21,7 @@ export function NumberInput({
min,
max,
precision = 2,
+ step = 0.1,
className = '',
}: NumberInputProps) {
const [isEditing, setIsEditing] = useState(false)
@@ -55,14 +57,14 @@ export function NumberInput({
const deltaX = moveEvent.clientX - startXRef.current
// Determine step size based on modifier keys
- let step = 0.1 // Default
+ let dragStep = step // Default from prop
if (moveEvent.shiftKey) {
- step = 1.0 // Coarse
+ dragStep = step * 10 // Coarse
} else if (moveEvent.altKey) {
- step = 0.01 // Fine
+ dragStep = step * 0.1 // Fine
}
- const deltaValue = deltaX * step
+ const deltaValue = deltaX * dragStep
const newValue = clamp(startValueRef.current + deltaValue)
const newFinalValue = Number.parseFloat(newValue.toFixed(precision))
@@ -143,7 +145,8 @@ export function NumberInput({
{isEditing ? (
{
- if (!projectId) {
- console.error('❌ No project ID found');
- return;
- }
- console.log('🎯 Generate thumbnail clicked for project:', projectId);
+ if (!projectId) return;
setIsGeneratingThumbnail(true);
emitter.emit('camera-controls:generate-thumbnail', { projectId });
- console.log('📤 Event emitted with project ID:', projectId);
- // Reset loading state after a delay (thumbnail generation is async)
setTimeout(() => setIsGeneratingThumbnail(false), 3000);
};
@@ -106,6 +100,16 @@ export function SettingsPanel() {
+ {activeProject?.thumbnail_url && (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+ )}
- {/* Optional Address Section */}
- {!showAddressSearch ? (
-