window building
This commit is contained in:
@@ -8,6 +8,7 @@ import { ItemPanel } from './item-panel'
|
|||||||
import { ReferencePanel } from './reference-panel'
|
import { ReferencePanel } from './reference-panel'
|
||||||
import { RoofPanel } from './roof-panel'
|
import { RoofPanel } from './roof-panel'
|
||||||
import { SlabPanel } from './slab-panel'
|
import { SlabPanel } from './slab-panel'
|
||||||
|
import { WindowPanel } from './window-panel'
|
||||||
|
|
||||||
export function PanelManager() {
|
export function PanelManager() {
|
||||||
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
||||||
@@ -33,6 +34,8 @@ export function PanelManager() {
|
|||||||
return <SlabPanel />
|
return <SlabPanel />
|
||||||
case 'ceiling':
|
case 'ceiling':
|
||||||
return <CeilingPanel />
|
return <CeilingPanel />
|
||||||
|
case 'window':
|
||||||
|
return <WindowPanel />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { type AnyNode, type AnyNodeId, type WindowNode, 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'
|
||||||
|
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<WindowNode>) => {
|
||||||
|
if (!selectedId) return
|
||||||
|
updateNode(selectedId as AnyNode['id'], updates)
|
||||||
|
// Mark dirty so window-system regenerates geometry
|
||||||
|
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
|
||||||
|
},
|
||||||
|
[selectedId, updateNode],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setSelection({ selectedIds: [] })
|
||||||
|
}, [setSelection])
|
||||||
|
|
||||||
|
if (!node || node.type !== 'window' || selectedIds.length !== 1) return null
|
||||||
|
|
||||||
|
const columns = node.columnRatios.length
|
||||||
|
const rows = node.rowRatios.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-auto fixed top-20 right-4 z-50 flex w-72 flex-col overflow-hidden rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between gap-2 border-b p-3">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<Image src="/icons/window.png" alt="" width={16} height={16} className="shrink-0 object-contain" />
|
||||||
|
<h2 className="font-semibold text-foreground text-sm truncate">
|
||||||
|
{node.name || `Window (${node.width}×${node.height}m)`}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground cursor-pointer"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||||
|
{/* Dimensions */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Dimensions
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Width"
|
||||||
|
value={Math.round(node.width * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ width: v })}
|
||||||
|
min={0.2}
|
||||||
|
precision={2}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Height"
|
||||||
|
value={Math.round(node.height * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ height: v })}
|
||||||
|
min={0.2}
|
||||||
|
precision={2}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Frame */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Frame
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Thickness"
|
||||||
|
value={Math.round(node.frameThickness * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ frameThickness: v })}
|
||||||
|
min={0.01}
|
||||||
|
precision={3}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Depth"
|
||||||
|
value={Math.round(node.frameDepth * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ frameDepth: v })}
|
||||||
|
min={0.01}
|
||||||
|
precision={3}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Grid
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Columns"
|
||||||
|
value={columns}
|
||||||
|
onChange={(v) => {
|
||||||
|
const n = Math.max(1, Math.min(8, Math.round(v)))
|
||||||
|
handleUpdate({ columnRatios: Array(n).fill(1) })
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
max={8}
|
||||||
|
precision={0}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Rows"
|
||||||
|
value={rows}
|
||||||
|
onChange={(v) => {
|
||||||
|
const n = Math.max(1, Math.min(8, Math.round(v)))
|
||||||
|
handleUpdate({ rowRatios: Array(n).fill(1) })
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
max={8}
|
||||||
|
precision={0}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(columns > 1 || rows > 1) && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Divider thickness"
|
||||||
|
value={Math.round(node.dividerThickness * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ dividerThickness: v })}
|
||||||
|
min={0.005}
|
||||||
|
precision={3}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sill */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Sill
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
checked={node.sill}
|
||||||
|
onCheckedChange={(checked) => handleUpdate({ sill: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{node.sill && (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Depth"
|
||||||
|
value={Math.round(node.sillDepth * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ sillDepth: v })}
|
||||||
|
min={0.01}
|
||||||
|
precision={3}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Thickness"
|
||||||
|
value={Math.round(node.sillThickness * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ sillThickness: v })}
|
||||||
|
min={0.005}
|
||||||
|
precision={3}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export type {
|
|||||||
SiteEvent,
|
SiteEvent,
|
||||||
SlabEvent,
|
SlabEvent,
|
||||||
WallEvent,
|
WallEvent,
|
||||||
|
WindowEvent,
|
||||||
ZoneEvent,
|
ZoneEvent,
|
||||||
} from './events/bus'
|
} from './events/bus'
|
||||||
// Events
|
// Events
|
||||||
@@ -22,12 +23,21 @@ export {
|
|||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useRegistry,
|
useRegistry,
|
||||||
} from './hooks/scene-registry/scene-registry'
|
} from './hooks/scene-registry/scene-registry'
|
||||||
|
export { pointInPolygon, spatialGridManager } from './hooks/spatial-grid/spatial-grid-manager'
|
||||||
export {
|
export {
|
||||||
initSpatialGridSync,
|
initSpatialGridSync,
|
||||||
resolveLevelId,
|
resolveLevelId,
|
||||||
} from './hooks/spatial-grid/spatial-grid-sync'
|
} from './hooks/spatial-grid/spatial-grid-sync'
|
||||||
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
|
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
|
||||||
export { pointInPolygon, spatialGridManager } from './hooks/spatial-grid/spatial-grid-manager'
|
// Asset storage
|
||||||
|
export { loadAssetUrl, saveAsset } from './lib/asset-storage'
|
||||||
|
// Space detection
|
||||||
|
export {
|
||||||
|
detectSpacesForLevel,
|
||||||
|
initSpaceDetectionSync,
|
||||||
|
type Space,
|
||||||
|
wallTouchesOthers,
|
||||||
|
} from './lib/space-detection'
|
||||||
// Schema
|
// Schema
|
||||||
export * from './schema'
|
export * from './schema'
|
||||||
export { default as useScene } from './store/use-scene'
|
export { default as useScene } from './store/use-scene'
|
||||||
@@ -38,9 +48,4 @@ export { RoofSystem } from './systems/roof/roof-system'
|
|||||||
export { SlabSystem } from './systems/slab/slab-system'
|
export { SlabSystem } from './systems/slab/slab-system'
|
||||||
export { WallSystem } from './systems/wall/wall-system'
|
export { WallSystem } from './systems/wall/wall-system'
|
||||||
export { WindowSystem } from './systems/window/window-system'
|
export { WindowSystem } from './systems/window/window-system'
|
||||||
|
|
||||||
export { isObject } from './utils/types'
|
export { isObject } from './utils/types'
|
||||||
// Asset storage
|
|
||||||
export { saveAsset, loadAssetUrl } from './lib/asset-storage'
|
|
||||||
// Space detection
|
|
||||||
export { detectSpacesForLevel, wallTouchesOthers, initSpaceDetectionSync, type Space } from './lib/space-detection'
|
|
||||||
|
|||||||
@@ -7,15 +7,25 @@ import useScene from '../../store/use-scene'
|
|||||||
|
|
||||||
const glassMaterial = new MeshStandardNodeMaterial({
|
const glassMaterial = new MeshStandardNodeMaterial({
|
||||||
name: 'glass',
|
name: 'glass',
|
||||||
color: 'lightgray',
|
color: 'lightblue',
|
||||||
roughness: 0.8,
|
roughness: 0.05,
|
||||||
metalness: 0,
|
metalness: 0.1,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.35,
|
opacity: 0.3,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const frameMaterial = new MeshStandardNodeMaterial({
|
||||||
|
name: 'window-frame',
|
||||||
|
color: '#e8e8e8',
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Invisible material for root mesh — used as selection hitbox only
|
||||||
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false })
|
||||||
|
|
||||||
export const WindowSystem = () => {
|
export const WindowSystem = () => {
|
||||||
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
||||||
const clearDirty = useScene((state) => state.clearDirty)
|
const clearDirty = useScene((state) => state.clearDirty)
|
||||||
@@ -45,17 +55,117 @@ export const WindowSystem = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addBox(
|
||||||
|
parent: THREE.Object3D,
|
||||||
|
material: THREE.Material,
|
||||||
|
w: number, h: number, d: number,
|
||||||
|
x: number, y: number, z: number,
|
||||||
|
) {
|
||||||
|
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material)
|
||||||
|
m.position.set(x, y, z)
|
||||||
|
parent.add(m)
|
||||||
|
}
|
||||||
|
|
||||||
function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
||||||
// Replace geometry with a box matching the overall window dimensions
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
|
||||||
mesh.material = glassMaterial
|
mesh.material = hitboxMaterial
|
||||||
|
|
||||||
// Sync transform from node (React may lag behind the system by a frame during drag)
|
// Sync transform from node (React may lag behind the system by a frame during drag)
|
||||||
mesh.position.set(node.position[0], node.position[1], node.position[2])
|
mesh.position.set(node.position[0], node.position[1], node.position[2])
|
||||||
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2])
|
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2])
|
||||||
|
|
||||||
// Update (or create) the named cutout mesh used by wall-system for CSG subtraction
|
// Dispose and remove all old visual children; preserve 'cutout'
|
||||||
|
for (const child of [...mesh.children]) {
|
||||||
|
if (child.name === 'cutout') continue
|
||||||
|
if (child instanceof THREE.Mesh) child.geometry.dispose()
|
||||||
|
mesh.remove(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
width, height, frameDepth, frameThickness,
|
||||||
|
columnRatios, rowRatios, dividerThickness,
|
||||||
|
sill, sillDepth, sillThickness,
|
||||||
|
} = node
|
||||||
|
|
||||||
|
const innerW = width - 2 * frameThickness
|
||||||
|
const innerH = height - 2 * frameThickness
|
||||||
|
|
||||||
|
// ── Frame members ──
|
||||||
|
// Top / bottom — full width
|
||||||
|
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0)
|
||||||
|
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0)
|
||||||
|
// Left / right — inner height to avoid corner overlap
|
||||||
|
addBox(mesh, frameMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0)
|
||||||
|
addBox(mesh, frameMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0)
|
||||||
|
|
||||||
|
// ── Pane grid ──
|
||||||
|
const numCols = columnRatios.length
|
||||||
|
const numRows = rowRatios.length
|
||||||
|
|
||||||
|
const usableW = innerW - (numCols - 1) * dividerThickness
|
||||||
|
const usableH = innerH - (numRows - 1) * dividerThickness
|
||||||
|
|
||||||
|
const colSum = columnRatios.reduce((a, b) => a + b, 0)
|
||||||
|
const rowSum = rowRatios.reduce((a, b) => a + b, 0)
|
||||||
|
const colWidths = columnRatios.map(r => (r / colSum) * usableW)
|
||||||
|
const rowHeights = rowRatios.map(r => (r / rowSum) * usableH)
|
||||||
|
|
||||||
|
// Compute column x-centers starting from left edge of inner area
|
||||||
|
const colXCenters: number[] = []
|
||||||
|
let cx = -innerW / 2
|
||||||
|
for (let c = 0; c < numCols; c++) {
|
||||||
|
colXCenters.push(cx + colWidths[c]! / 2)
|
||||||
|
cx += colWidths[c]!
|
||||||
|
if (c < numCols - 1) cx += dividerThickness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute row y-centers starting from bottom edge of inner area
|
||||||
|
const rowYCenters: number[] = []
|
||||||
|
let cy = -innerH / 2
|
||||||
|
for (let r = 0; r < numRows; r++) {
|
||||||
|
rowYCenters.push(cy + rowHeights[r]! / 2)
|
||||||
|
cy += rowHeights[r]!
|
||||||
|
if (r < numRows - 1) cy += dividerThickness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column dividers — full inner height
|
||||||
|
cx = -innerW / 2
|
||||||
|
for (let c = 0; c < numCols - 1; c++) {
|
||||||
|
cx += colWidths[c]!
|
||||||
|
addBox(mesh, frameMaterial, dividerThickness, innerH, frameDepth, cx + dividerThickness / 2, 0, 0)
|
||||||
|
cx += dividerThickness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row dividers — per column width, so they don't overlap column dividers
|
||||||
|
cy = -innerH / 2
|
||||||
|
for (let r = 0; r < numRows - 1; r++) {
|
||||||
|
cy += rowHeights[r]!
|
||||||
|
const divY = cy + dividerThickness / 2
|
||||||
|
for (let c = 0; c < numCols; c++) {
|
||||||
|
addBox(mesh, frameMaterial, colWidths[c]!, dividerThickness, frameDepth, colXCenters[c]!, divY, 0)
|
||||||
|
}
|
||||||
|
cy += dividerThickness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glass panes
|
||||||
|
const glassDepth = Math.max(0.004, frameDepth * 0.08)
|
||||||
|
for (let c = 0; c < numCols; c++) {
|
||||||
|
for (let r = 0; r < numRows; r++) {
|
||||||
|
addBox(mesh, glassMaterial, colWidths[c]!, rowHeights[r]!, glassDepth, colXCenters[c]!, rowYCenters[r]!, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sill ──
|
||||||
|
if (sill) {
|
||||||
|
const sillW = width + sillDepth * 0.4 // slightly wider than frame
|
||||||
|
// Protrudes from the front face of the frame (+Z)
|
||||||
|
const sillZ = frameDepth / 2 + sillDepth / 2
|
||||||
|
addBox(mesh, frameMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
|
||||||
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
|
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
|
||||||
if (!cutout) {
|
if (!cutout) {
|
||||||
cutout = new THREE.Mesh()
|
cutout = new THREE.Mesh()
|
||||||
@@ -63,7 +173,6 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
|||||||
mesh.add(cutout)
|
mesh.add(cutout)
|
||||||
}
|
}
|
||||||
cutout.geometry.dispose()
|
cutout.geometry.dispose()
|
||||||
// Extends 1m through the wall so the CSG brush covers full wall thickness
|
|
||||||
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
|
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
|
||||||
cutout.visible = false;
|
cutout.visible = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useRegistry, type WindowNode } from '@pascal-app/core'
|
import { useRegistry, type WindowNode } from '@pascal-app/core'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import type { Mesh } from 'three'
|
import type { Mesh } from 'three'
|
||||||
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
|
|
||||||
export const WindowRenderer = ({ node }: { node: WindowNode }) => {
|
export const WindowRenderer = ({ node }: { node: WindowNode }) => {
|
||||||
const ref = useRef<Mesh>(null!)
|
const ref = useRef<Mesh>(null!)
|
||||||
|
|
||||||
useRegistry(node.id, 'window', ref)
|
useRegistry(node.id, 'window', ref)
|
||||||
|
const handlers = useNodeEvents(node, 'window')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -15,6 +17,7 @@ export const WindowRenderer = ({ node }: { node: WindowNode }) => {
|
|||||||
visible={node.visible}
|
visible={node.visible}
|
||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
|
{...handlers}
|
||||||
>
|
>
|
||||||
{/* WindowSystem replaces this geometry each time the node is dirty */}
|
{/* WindowSystem replaces this geometry each time the node is dirty */}
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import {
|
|||||||
type SlabNode,
|
type SlabNode,
|
||||||
type WallEvent,
|
type WallEvent,
|
||||||
type WallNode,
|
type WallNode,
|
||||||
|
type WindowEvent,
|
||||||
|
type WindowNode,
|
||||||
type ZoneEvent,
|
type ZoneEvent,
|
||||||
type ZoneNode,
|
type ZoneNode,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import type { ThreeEvent } from '@react-three/fiber'
|
import type { ThreeEvent } from '@react-three/fiber'
|
||||||
import useViewer from '../store/use-viewer';
|
import useViewer from '../store/use-viewer'
|
||||||
|
|
||||||
type NodeConfig = {
|
type NodeConfig = {
|
||||||
site: { node: SiteNode; event: SiteEvent }
|
site: { node: SiteNode; event: SiteEvent }
|
||||||
@@ -33,6 +35,7 @@ type NodeConfig = {
|
|||||||
slab: { node: SlabNode; event: SlabEvent }
|
slab: { node: SlabNode; event: SlabEvent }
|
||||||
ceiling: { node: CeilingNode; event: CeilingEvent }
|
ceiling: { node: CeilingNode; event: CeilingEvent }
|
||||||
roof: { node: RoofNode; event: RoofEvent }
|
roof: { node: RoofNode; event: RoofEvent }
|
||||||
|
window: { node: WindowNode; event: WindowEvent }
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeType = keyof NodeConfig
|
type NodeType = keyof NodeConfig
|
||||||
@@ -69,10 +72,25 @@ export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], t
|
|||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
emit('click', e)
|
emit('click', e)
|
||||||
},
|
},
|
||||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('enter', e)},
|
onPointerEnter: (e: ThreeEvent<PointerEvent>) => {
|
||||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('leave', e)},
|
if (useViewer.getState().cameraDragging) return
|
||||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('move', e)},
|
emit('enter', e)
|
||||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('double-click', e)},
|
},
|
||||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => {if (useViewer.getState().cameraDragging) return; emit('context-menu', e)},
|
onPointerLeave: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (useViewer.getState().cameraDragging) return
|
||||||
|
emit('leave', e)
|
||||||
|
},
|
||||||
|
onPointerMove: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (useViewer.getState().cameraDragging) return
|
||||||
|
emit('move', e)
|
||||||
|
},
|
||||||
|
onDoubleClick: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (useViewer.getState().cameraDragging) return
|
||||||
|
emit('double-click', e)
|
||||||
|
},
|
||||||
|
onContextMenu: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (useViewer.getState().cameraDragging) return
|
||||||
|
emit('context-menu', e)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user