From 5deb0b363e244a0daf6ca1fbd6ed4b34ece88181 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 07:47:03 +0900 Subject: [PATCH 01/11] fix render priority for item-system --- packages/core/src/systems/item/item-system.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/systems/item/item-system.tsx b/packages/core/src/systems/item/item-system.tsx index b2876c29..dbfbf7e3 100644 --- a/packages/core/src/systems/item/item-system.tsx +++ b/packages/core/src/systems/item/item-system.tsx @@ -3,7 +3,7 @@ import type * as THREE from 'three' import { sceneRegistry } from '../../hooks/scene-registry/scene-registry' import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager' import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync' -import { getScaledDimensions, type AnyNodeId, type ItemNode, type WallNode } from '../../schema' +import { type AnyNodeId, getScaledDimensions, type ItemNode, type WallNode } from '../../schema' import useScene from '../../store/use-scene' // ============================================================================ @@ -32,7 +32,7 @@ export const ItemSystem = () => { if (parentWall && parentWall.type === 'wall') { const wallThickness = (parentWall as WallNode).thickness ?? 0.1 const side = item.side === 'front' ? 1 : -1 - mesh.position.z = (wallThickness / 2) * side; + mesh.position.z = (wallThickness / 2) * side } } else if (!item.asset.attachTo) { // If parented to another item (surface placement), R3F handles positioning via the hierarchy @@ -51,8 +51,8 @@ export const ItemSystem = () => { } clearDirty(id as AnyNodeId) - }, 2) - }) + }) + }, 2) return null } From 3791739b5bce546f3e597bd3c56caeee91dd2f48 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 08:19:49 +0900 Subject: [PATCH 02/11] mark nodes as dirty after deletion --- packages/core/src/store/actions/node-actions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/store/actions/node-actions.ts b/packages/core/src/store/actions/node-actions.ts index 5ced050b..f42c24cf 100644 --- a/packages/core/src/store/actions/node-actions.ts +++ b/packages/core/src/store/actions/node-actions.ts @@ -152,6 +152,10 @@ export const deleteNodesAction = ( return { nodes: nextNodes, rootNodeIds: nextRootIds } }) - // Notify systems that the parent has changed (e.g. Wall needs to fill a window hole) - parentsToMarkDirty.forEach((pId) => get().markDirty(pId)) + + // Trigger a full scene re-validation after deleting node (as deleting a slab can cause widespread changes to level elevations) + const currentNodes = get().nodes + Object.values(currentNodes).forEach((node) => { + get().markDirty(node.id) + }) } From 20f5f7ab4ac0e2ee2b3b15805f0b92b01948392c Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 10:27:51 +0900 Subject: [PATCH 03/11] fix wall elevation with slab holes --- .../spatial-grid/spatial-grid-manager.ts | 53 +++++++++++++++---- .../hooks/spatial-grid/spatial-grid-sync.ts | 2 +- packages/core/src/store/use-scene.ts | 1 - 3 files changed, 43 insertions(+), 13 deletions(-) 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 ebd5e6a9..945ced5c 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -201,6 +201,21 @@ export function wallOverlapsPolygon( const nz = (dz / len) * step if (pointInPolygon(start[0] + nx, start[1] + nz, polygon)) return true if (pointInPolygon(end[0] - nx, end[1] - nz, polygon)) return true + + // Also nudge perpendicular to the wall (into the slab interior) for walls that + // lie exactly on the slab boundary. The along-wall nudge keeps points on the + // boundary where pointInPolygon is unreliable; a perpendicular inward nudge + // moves the point clearly inside (or outside) the polygon. + // Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge. + const PERP_STEP = 1e-4 + const pnx = (-nz / step) * PERP_STEP // perpendicular left + const pnz = (nx / step) * PERP_STEP + for (const t of [0.25, 0.5, 0.75]) { + const bx = start[0] + dx * t + const bz = start[1] + dz * t + if (pointInPolygon(bx + pnx, bz + pnz, polygon)) return true + if (pointInPolygon(bx - pnx, bz - pnz, polygon)) return true + } } // Check if midpoint is inside (catches walls crossing through) @@ -557,26 +572,42 @@ export class SpatialGridManager { let maxElevation = -Infinity for (const slab of slabMap.values()) { if (slab.polygon.length < 3) continue - if (wallOverlapsPolygon(start, end, slab.polygon)) { - // Check if wall midpoint is in a hole (if so, ignore this slab) + if (!wallOverlapsPolygon(start, end, slab.polygon)) continue + + const holes = slab.holes || [] + if (holes.length === 0) { + // No holes: wall is on this slab + const elevation = slab.elevation ?? 0.05 + if (elevation > maxElevation) maxElevation = elevation + continue + } + + // Sample multiple points along the wall to check whether any portion lies on + // solid slab (not inside any hole). Checking only the midpoint fails when the + // midpoint falls in a staircase hole but the wall's endpoints are on solid slab. + const dx = end[0] - start[0] + const dz = end[1] - start[1] + let hasValidPoint = false + for (const t of [0, 0.25, 0.5, 0.75, 1]) { + const px = start[0] + dx * t + const pz = start[1] + dz * t let inHole = false - const midX = (start[0] + end[0]) / 2 - const midZ = (start[1] + end[1]) / 2 - const holes = slab.holes || [] for (const hole of holes) { - if (hole.length >= 3 && pointInPolygon(midX, midZ, hole)) { + if (hole.length >= 3 && pointInPolygon(px, pz, hole)) { inHole = true break } } - if (!inHole) { - const elevation = slab.elevation ?? 0.05 - if (elevation > maxElevation) { - maxElevation = elevation - } + hasValidPoint = true + break } } + + if (hasValidPoint) { + const elevation = slab.elevation ?? 0.05 + if (elevation > maxElevation) maxElevation = elevation + } } return maxElevation === -Infinity ? 0 : maxElevation } diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts index 9566159e..c6505eb3 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts @@ -85,7 +85,7 @@ export function initSpatialGridSync() { } } } else if (node.type === 'slab' && prev.type === 'slab') { - if (node.polygon !== prev.polygon || node.elevation !== prev.elevation) { + if (node.polygon !== prev.polygon || node.elevation !== prev.elevation || node.holes !== prev.holes) { const levelId = resolveLevelId(node, state.nodes) spatialGridManager.handleNodeUpdated(node, levelId) diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index 3a970b79..3f19f328 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -167,7 +167,6 @@ const useScene: UseSceneStore = create()( rootNodeIds: state.rootNodeIds, }), merge: (persistedState, currentState) => { - console.log('merge calling...', persistedState, currentState) const persisted = persistedState as Partial // Backward compat: add default scale to item nodes saved before scale was added if (persisted.nodes) { From 28b44fe92d4cecc8dedd6535cbf62d59a01fcfe8 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 12:46:27 +0900 Subject: [PATCH 04/11] fix zones --- apps/editor/components/systems/zone/zone-system.tsx | 11 +++++++++-- .../src/components/renderers/zone/zone-renderer.tsx | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/editor/components/systems/zone/zone-system.tsx b/apps/editor/components/systems/zone/zone-system.tsx index 2c775526..1c5e2cf6 100644 --- a/apps/editor/components/systems/zone/zone-system.tsx +++ b/apps/editor/components/systems/zone/zone-system.tsx @@ -1,4 +1,4 @@ -import { type ZoneNode, sceneRegistry, useScene } from '@pascal-app/core' +import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useEffect } from 'react' import useEditor from '@/store/use-editor' @@ -24,10 +24,17 @@ export const ZoneSystem = () => { const hideInSoloMode = levelMode === 'solo' && selectedLevelId && !isOnSelectedLevel obj.visible = visible + const label = obj.getObjectByName('label') if (label) { // Hide label if zone layer is off OR if in solo mode on a different level - label.position.y = (visible && !hideInSoloMode) ? 1 : -1000 + const showLabel = visible && !hideInSoloMode; + const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined + if (showLabel && labelPosition) { + label.position.set(...labelPosition) + } else { + label.position.set(-9999, -9999, -9999) + } } }) }, [structureLayer, levelMode, selectedLevelId]) diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index 7fe2496a..22e3c128 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -177,7 +177,9 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { } return ( - + Date: Tue, 24 Feb 2026 12:46:33 +0900 Subject: [PATCH 05/11] fix radio --- apps/editor/components/pascal-radio.tsx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/editor/components/pascal-radio.tsx b/apps/editor/components/pascal-radio.tsx index f02dbf9b..30ee227e 100644 --- a/apps/editor/components/pascal-radio.tsx +++ b/apps/editor/components/pascal-radio.tsx @@ -75,6 +75,15 @@ export function PascalRadio() { // Calculate effective volume (masterVolume * radioVolume, both are 0-100) const effectiveVolume = (masterVolume / 100) * (radioVolume / 100) + // Keep a ref so the track-init effect can read current volume/muted/isPlaying + // without those values being part of its dependency array (which would restart the song). + const effectiveVolumeRef = useRef(effectiveVolume) + const mutedRef = useRef(muted) + const isPlayingRef = useRef(isPlaying) + effectiveVolumeRef.current = effectiveVolume + mutedRef.current = muted + isPlayingRef.current = isPlaying + const handleNext = useCallback(() => { setCurrentTrackIndex((prev) => (prev + 1) % shuffledPlaylist.length) }, [shuffledPlaylist.length]) @@ -83,32 +92,29 @@ export function PascalRadio() { setCurrentTrackIndex((prev) => (prev - 1 + shuffledPlaylist.length) % shuffledPlaylist.length) }, [shuffledPlaylist.length]) - // Initialize Howler when track changes + // Initialize Howler only when the track changes — not on volume/mute/play-state changes. + // Volume and mute are handled by the separate effect below. useEffect(() => { - // Clean up previous sound if (soundRef.current) { soundRef.current.unload() } - const wasPlaying = isPlaying + const wasPlaying = isPlayingRef.current - // Create new sound soundRef.current = new Howl({ src: [currentTrack.file], - volume: muted ? 0 : effectiveVolume, + volume: mutedRef.current ? 0 : effectiveVolumeRef.current, onend: handleNext, }) - // If was playing, play new track - if (wasPlaying && !muted) { + if (wasPlaying && !mutedRef.current) { soundRef.current?.play() } return () => { soundRef.current?.unload() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleNext, currentTrack.file, muted, isPlaying, effectiveVolume]) + }, [handleNext, currentTrack.file]) // Update volume when settings change useEffect(() => { From 0dea6a22055b3be6f7c0efa5adc06d96f02cd324 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 13:06:45 +0900 Subject: [PATCH 06/11] fix zones in viewer too --- apps/editor/app/viewer/[id]/viewer-zone-system.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/editor/app/viewer/[id]/viewer-zone-system.tsx b/apps/editor/app/viewer/[id]/viewer-zone-system.tsx index 5f21786e..80b48ca4 100644 --- a/apps/editor/app/viewer/[id]/viewer-zone-system.tsx +++ b/apps/editor/app/viewer/[id]/viewer-zone-system.tsx @@ -1,6 +1,6 @@ 'use client' -import { type ZoneNode, sceneRegistry, useScene } from '@pascal-app/core' +import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useFrame } from '@react-three/fiber' @@ -28,7 +28,13 @@ export const ViewerZoneSystem = () => { // Also hide the label const label = obj.getObjectByName('label') if (label) { - label.position.y = shouldShow ? 1 : -1000 + // Hide label if zone layer is off OR if in solo mode on a different level + const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined + if (shouldShow && labelPosition) { + label.position.set(...labelPosition) + } else { + label.position.set(-9999, -9999, -9999) + } } }) }) From 0863f5e6325a57e6db945c5f797c6c8c86ccc491 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 13:07:53 +0900 Subject: [PATCH 07/11] fix z-index viewer --- .../editor/app/viewer/[id]/viewer-overlay.tsx | 474 ++++++++++-------- 1 file changed, 265 insertions(+), 209 deletions(-) diff --git a/apps/editor/app/viewer/[id]/viewer-overlay.tsx b/apps/editor/app/viewer/[id]/viewer-overlay.tsx index 0cbb053c..49c8fd5f 100644 --- a/apps/editor/app/viewer/[id]/viewer-overlay.tsx +++ b/apps/editor/app/viewer/[id]/viewer-overlay.tsx @@ -1,9 +1,26 @@ 'use client' -import { type AnyNode, type AnyNodeId, type BuildingNode, type LevelNode, type ZoneNode, useScene } from '@pascal-app/core' +import { + type AnyNode, + type AnyNodeId, + type BuildingNode, + type LevelNode, + useScene, + type ZoneNode, +} from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' +import { + ArrowLeft, + Box, + ChevronRight, + Diamond, + Eye, + EyeOff, + Image, + Layers, + Layers2, +} from 'lucide-react' import Link from 'next/link' -import { ArrowLeft, Box, ChevronRight, Diamond, Eye, EyeOff, Image, Layers, Layers2 } from 'lucide-react' import type { ProjectOwner } from '@/features/community/lib/projects/types' const getNodeName = (node: AnyNode): string => { @@ -23,7 +40,12 @@ interface ViewerOverlayProps { canShowGuides?: boolean } -export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShowGuides = true }: ViewerOverlayProps) => { +export const ViewerOverlay = ({ + projectName, + owner, + canShowScans = true, + canShowGuides = true, +}: ViewerOverlayProps) => { const selection = useViewer((s) => s.selection) const nodes = useScene((s) => s.nodes) const showScans = useViewer((s) => s.showScans) @@ -32,20 +54,24 @@ export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShow const levelMode = useViewer((s) => s.levelMode) const wallMode = useViewer((s) => s.wallMode) - const building = selection.buildingId ? (nodes[selection.buildingId] as BuildingNode | undefined) : null + const building = selection.buildingId + ? (nodes[selection.buildingId] as BuildingNode | undefined) + : null const level = selection.levelId ? (nodes[selection.levelId] as LevelNode | undefined) : null const zone = selection.zoneId ? (nodes[selection.zoneId] as ZoneNode | undefined) : null // Get the first selected item (if any) - const selectedNode = selection.selectedIds.length > 0 - ? (nodes[selection.selectedIds[0] as AnyNodeId] as AnyNode | undefined) - : null + const selectedNode = + selection.selectedIds.length > 0 + ? (nodes[selection.selectedIds[0] as AnyNodeId] as AnyNode | undefined) + : null // Get all levels for the selected building - const levels = building?.children - .map((id) => nodes[id as AnyNodeId] as LevelNode | undefined) - .filter((n): n is LevelNode => n?.type === 'level') - .sort((a, b) => a.level - b.level) ?? [] + const levels = + building?.children + .map((id) => nodes[id as AnyNodeId] as LevelNode | undefined) + .filter((n): n is LevelNode => n?.type === 'level') + .sort((a, b) => a.level - b.level) ?? [] const handleLevelClick = (levelId: LevelNode['id']) => { // When switching levels, deselect zone and items @@ -68,219 +94,249 @@ export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShow return ( <> - {/* Unified top-left card */} -
-
- {/* Project info + back */} -
- - - -
-
- {projectName || 'Untitled'} + {/* Unified top-left card */} +
+
+ {/* Project info + back */} +
+ + + +
+
+ {projectName || 'Untitled'} +
+ {owner?.username && ( + + @{owner.username} + + )}
- {owner?.username && ( - - @{owner.username} - - )}
-
- {/* Breadcrumb — only shown when navigated into a building */} - {building && ( -
-
- - - {building && ( - <> - + {/* Breadcrumb — only shown when navigated into a building */} + {building && ( +
+
- - )} - {level && ( - <> - - - - )} + {building && ( + <> + + + + )} - {zone && ( - <> - - - {zone.name} - - - )} + {level && ( + <> + + + + )} - {selectedNode && zone && ( - <> - - {getNodeName(selectedNode)} - - )} -
-
- )} -
+ {zone && ( + <> + + + {zone.name} + + + )} - {/* Level List (only when building is selected) */} - {building && levels.length > 0 && ( -
- Levels - {levels.map((lvl) => ( - - ))} -
- )} -
- - {/* Controls Panel - Top Right */} -
- {/* Visibility Controls */} - {(canShowScans || canShowGuides) && ( -
- Visibility - {canShowScans && ( - - )} - {canShowGuides && ( - - )} -
- )} - - {/* Camera Mode */} -
- Camera -
+
)} - {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'} - +
+ + {/* Level List (only when building is selected) */} + {building && levels.length > 0 && ( +
+ Levels + {levels.map((lvl) => ( + + ))} +
+ )}
- {/* Level Mode */} -
- Level Mode - - - -
+ {/* Controls Panel - Top Right */} +
+ {/* Visibility Controls */} + {(canShowScans || canShowGuides) && ( +
+ Visibility + {canShowScans && ( + + )} + {canShowGuides && ( + + )} +
+ )} - {/* Wall Mode */} -
- Wall Mode - - - + {/* Camera Mode */} +
+ Camera + +
+ + {/* Level Mode */} +
+ Level Mode + + + +
+ + {/* Wall Mode */} +
+ Wall Mode + + + +
-
) } From 81ede241d5b11aa1070755a0e09a17f66317b9e5 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 14:48:59 +0900 Subject: [PATCH 08/11] save site + camera build position --- .../viewer/[id]/viewer-camera-controls.tsx | 32 ++- .../ui/sidebar/panels/site-panel/index.tsx | 196 +++++++++++++++++- 2 files changed, 208 insertions(+), 20 deletions(-) diff --git a/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx b/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx index ab59152b..17f1d745 100644 --- a/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx +++ b/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx @@ -44,23 +44,33 @@ export const ViewerCameraControls = () => { controls.current.setLookAt(30, 30, 30, 0, 0, 0, false) } - if (!targetNodeId) return - const node = nodes[targetNodeId] + let node = targetNodeId ? nodes[targetNodeId] : null; + if (!targetNodeId) { + const site = Object.values(nodes).find((n) => n.type === 'site') + node = site || null + } if (!node) return // Check if node has a saved camera if (node.camera) { + const { position, target } = node.camera - controls.current.setLookAt( - position[0], - position[1], - position[2], - target[0], - target[1], - target[2], - true, - ) + requestAnimationFrame(() => { + controls.current.setLookAt( + position[0], + position[1], + position[2], + target[0], + target[1], + target[2], + true, + ) + }) + return + } + if (!targetNodeId) { + // No selection and no site - do nothing return } 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 54581749..edd234be 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -1,4 +1,5 @@ import { + type AnyNodeId, type BuildingNode, emitter, LevelNode, @@ -225,20 +226,122 @@ function PropertyLineSection() { // SITE PHASE VIEW - Property line + building buttons // ============================================================================ +function CameraPopover({ + nodeId, + hasCamera, + open, + onOpenChange, + buttonClassName, +}: { + nodeId: AnyNodeId; + hasCamera: boolean; + open: boolean; + onOpenChange: (open: boolean) => void; + buttonClassName?: string; +}) { + const updateNode = useScene((state) => state.updateNode); + return ( + + + + + e.stopPropagation()} + > +
+ {hasCamera && ( + + )} + + {hasCamera && ( + + )} +
+
+
+ ); +} + function SitePhaseView() { const nodes = useScene((state) => state.nodes); const rootNodeIds = useScene((state) => state.rootNodeIds); + const updateNode = useScene((state) => state.updateNode); const selectedBuildingId = useViewer((state) => state.selection.buildingId); const setSelection = useViewer((state) => state.setSelection); + const [siteCameraOpen, setSiteCameraOpen] = useState(false); + const [buildingCameraOpen, setBuildingCameraOpen] = useState(null); - // Get site node and its building children const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null; const buildings = (siteNode?.type === 'site' ? siteNode.children : []) - .map((child) => typeof child === 'string' ? nodes[child] : child) + .map((child) => { + const id = typeof child === 'string' ? child : child.id; + return nodes[id] as BuildingNode | undefined; + }) .filter((node): node is BuildingNode => node?.type === "building"); return (
+ {/* Site row */} + {siteNode && ( +
+
+ + {siteNode.name || "Site"} +
+ +
+ )} {buildings.length === 0 ? (
@@ -247,19 +350,91 @@ function SitePhaseView() { ) : (
{buildings.map((building) => ( - + + setBuildingCameraOpen(open ? building.id : null)} + > + + + + e.stopPropagation()} + > +
+ {building.camera && ( + + )} + + {building.camera && ( + + )} +
+
+
+
))}
)} @@ -280,7 +455,10 @@ function BuildingSelector() { // Get site node and its building children const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null; const buildings = (siteNode?.type === 'site' ? siteNode.children : []) - .map((child) => typeof child === 'string' ? nodes[child] : child) + .map((child) => { + const id = typeof child === 'string' ? child : child.id; + return nodes[id] as BuildingNode | undefined; + }) .filter((node): node is BuildingNode => node?.type === "building"); const selectedBuilding = selectedBuildingId From 14708c041a4cce96b9719756111af2a5dc56ebaa Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 16:09:03 +0900 Subject: [PATCH 09/11] fix tools visibility --- .../components/tools/ceiling/ceiling-tool.tsx | 46 ++-- .../components/tools/roof/roof-tool.tsx | 228 +++++++++--------- .../components/tools/shared/cursor-sphere.tsx | 19 ++ .../components/tools/slab/slab-tool.tsx | 15 +- .../components/tools/wall/wall-tool.tsx | 6 +- .../components/tools/zone/zone-tool.tsx | 19 +- .../renderers/ceiling/ceiling-renderer.tsx | 7 +- 7 files changed, 170 insertions(+), 170 deletions(-) create mode 100644 apps/editor/components/tools/shared/cursor-sphere.tsx diff --git a/apps/editor/components/tools/ceiling/ceiling-tool.tsx b/apps/editor/components/tools/ceiling/ceiling-tool.tsx index 3a56a74b..89567f85 100644 --- a/apps/editor/components/tools/ceiling/ceiling-tool.tsx +++ b/apps/editor/components/tools/ceiling/ceiling-tool.tsx @@ -2,8 +2,9 @@ import { CeilingNode, emitter, type GridEvent, type LevelNode, useScene } from ' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three' -import useEditor from '@/store/use-editor' import { sfxEmitter } from '@/lib/sfx-bus' +import useEditor from '@/store/use-editor' +import { CursorSphere } from '../shared/cursor-sphere' const CEILING_HEIGHT = 2.52 const GRID_OFFSET = 0.02 @@ -97,12 +98,19 @@ export const CeilingTool: React.FC = () => { // Calculate snapped display position (bypass snap when Shift is held) const lastPoint = points[points.length - 1] - const displayPoint = (shiftPressed.current || !lastPoint) ? gridPosition : calculateSnapPoint(lastPoint, gridPosition) + const displayPoint = + shiftPressed.current || !lastPoint + ? gridPosition + : calculateSnapPoint(lastPoint, gridPosition) setSnappedCursorPosition(displayPoint) // Play snap sound when the snapped position actually changes (only when drawing) - if (points.length > 0 && previousSnappedPointRef.current && - (displayPoint[0] !== previousSnappedPointRef.current[0] || displayPoint[1] !== previousSnappedPointRef.current[1])) { + if ( + points.length > 0 && + previousSnappedPointRef.current && + (displayPoint[0] !== previousSnappedPointRef.current[0] || + displayPoint[1] !== previousSnappedPointRef.current[1]) + ) { sfxEmitter.emit('sfx:grid-snap') } @@ -150,8 +158,12 @@ export const CeilingTool: React.FC = () => { setPoints([]) } - const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = true } - const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = false } + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = true + } + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = false + } document.addEventListener('keydown', onKeyDown) document.addEventListener('keyup', onKeyUp) @@ -242,15 +254,12 @@ export const CeilingTool: React.FC = () => { return ( {/* Cursor at ceiling height */} - - - - + {/* Grid-level cursor indicator */} - + - + {/* Preview fill */} @@ -294,14 +303,11 @@ export const CeilingTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => ( - - - - + ))} ) diff --git a/apps/editor/components/tools/roof/roof-tool.tsx b/apps/editor/components/tools/roof/roof-tool.tsx index 9f351007..6698ae75 100644 --- a/apps/editor/components/tools/roof/roof-tool.tsx +++ b/apps/editor/components/tools/roof/roof-tool.tsx @@ -1,37 +1,44 @@ -import { emitter, type GridEvent, useScene, RoofNode, type LevelNode, type AnyNode } from "@pascal-app/core"; -import { useViewer } from "@pascal-app/viewer"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { BufferGeometry, DoubleSide, type Line, Vector3 } from "three"; -import useEditor from "@/store/use-editor"; -import { sfxEmitter } from '@/lib/sfx-bus'; +import { + type AnyNode, + emitter, + type GridEvent, + type LevelNode, + RoofNode, + useScene, +} from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useEffect, useMemo, useRef, useState } from 'react' +import { BufferGeometry, DoubleSide, type Line, Vector3 } from 'three' +import { sfxEmitter } from '@/lib/sfx-bus' +import useEditor from '@/store/use-editor' // Default roof dimensions -const DEFAULT_HEIGHT = 1.5; -const PREVIEW_LINE_HEIGHT = 0.03; // Very thin preview +const DEFAULT_HEIGHT = 1.5 +const PREVIEW_LINE_HEIGHT = 0.03 // Very thin preview /** * Creates a roof with the given corners */ const commitRoofPlacement = ( - levelId: LevelNode["id"], + levelId: LevelNode['id'], corner1: [number, number, number], - corner2: [number, number, number] -): RoofNode["id"] => { - const { createNode, nodes } = useScene.getState(); + corner2: [number, number, number], +): RoofNode['id'] => { + const { createNode, nodes } = useScene.getState() // Calculate center position and dimensions from corners - const centerX = (corner1[0] + corner2[0]) / 2; - const centerZ = (corner1[2] + corner2[2]) / 2; + const centerX = (corner1[0] + corner2[0]) / 2 + const centerZ = (corner1[2] + corner2[2]) / 2 - const length = Math.abs(corner2[0] - corner1[0]); - const width = Math.abs(corner2[2] - corner1[2]); + const length = Math.abs(corner2[0] - corner1[0]) + const width = Math.abs(corner2[2] - corner1[2]) // Split width evenly between left and right slopes - const slopeWidth = Math.max(width / 2, 0.5); + const slopeWidth = Math.max(width / 2, 0.5) // Count existing roofs for naming - const roofCount = Object.values(nodes).filter((n) => n.type === "roof").length; - const name = `Roof ${roofCount + 1}`; + const roofCount = Object.values(nodes).filter((n) => n.type === 'roof').length + const name = `Roof ${roofCount + 1}` const roof = RoofNode.parse({ name, @@ -40,151 +47,153 @@ const commitRoofPlacement = ( height: DEFAULT_HEIGHT, leftWidth: slopeWidth, rightWidth: slopeWidth, - }); + }) - createNode(roof, levelId); - sfxEmitter.emit('sfx:structure-build'); - return roof.id; -}; + createNode(roof, levelId) + sfxEmitter.emit('sfx:structure-build') + return roof.id +} type PreviewState = { - corner1: [number, number, number] | null; - cursorPosition: [number, number, number]; - levelY: number; -}; + corner1: [number, number, number] | null + cursorPosition: [number, number, number] + levelY: number +} export const RoofTool: React.FC = () => { - const outlineRef = useRef(null!); - const currentLevelId = useViewer((state) => state.selection.levelId); - const setSelection = useViewer((state) => state.setSelection); - const setTool = useEditor((state) => state.setTool); - const setMode = useEditor((state) => state.setMode); + const outlineRef = useRef(null!) + const currentLevelId = useViewer((state) => state.selection.levelId) + const setSelection = useViewer((state) => state.setSelection) + const setTool = useEditor((state) => state.setTool) + const setMode = useEditor((state) => state.setMode) - const corner1Ref = useRef<[number, number, number] | null>(null); - const previousGridPosRef = useRef<[number, number] | null>(null); + const corner1Ref = useRef<[number, number, number] | null>(null) + const previousGridPosRef = useRef<[number, number] | null>(null) const [preview, setPreview] = useState({ corner1: null, cursorPosition: [0, 0, 0], levelY: 0, - }); + }) useEffect(() => { - if (!currentLevelId) return; + if (!currentLevelId) return // Initialize outline geometry - outlineRef.current.geometry = new BufferGeometry(); + outlineRef.current.geometry = new BufferGeometry() - const updateOutline = (corner1: [number, number, number], corner2: [number, number, number]) => { - const y = corner1[1] + PREVIEW_LINE_HEIGHT; + const updateOutline = ( + corner1: [number, number, number], + corner2: [number, number, number], + ) => { + const y = corner1[1] + PREVIEW_LINE_HEIGHT const points = [ new Vector3(corner1[0], y, corner1[2]), new Vector3(corner2[0], y, corner1[2]), new Vector3(corner2[0], y, corner2[2]), new Vector3(corner1[0], y, corner2[2]), new Vector3(corner1[0], y, corner1[2]), // Close the loop - ]; - outlineRef.current.geometry.dispose(); - outlineRef.current.geometry = new BufferGeometry().setFromPoints(points); - outlineRef.current.visible = true; - }; + ] + outlineRef.current.geometry.dispose() + outlineRef.current.geometry = new BufferGeometry().setFromPoints(points) + outlineRef.current.visible = true + } const onGridMove = (event: GridEvent) => { // Snap to 0.5 grid - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - const y = event.position[1]; + const gridX = Math.round(event.position[0] * 2) / 2 + const gridZ = Math.round(event.position[2] * 2) / 2 + const y = event.position[1] - const cursorPosition: [number, number, number] = [gridX, y, gridZ]; + const cursorPosition: [number, number, number] = [gridX, y, gridZ] // Play snap sound when grid position changes (only when placing) - if (corner1Ref.current && previousGridPosRef.current && - (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) { - sfxEmitter.emit('sfx:grid-snap'); + if ( + corner1Ref.current && + previousGridPosRef.current && + (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1]) + ) { + sfxEmitter.emit('sfx:grid-snap') } - previousGridPosRef.current = [gridX, gridZ]; + previousGridPosRef.current = [gridX, gridZ] setPreview({ corner1: corner1Ref.current, cursorPosition, levelY: y, - }); + }) // Update outline if we have first corner if (corner1Ref.current) { - updateOutline(corner1Ref.current, cursorPosition); + updateOutline(corner1Ref.current, cursorPosition) } - }; + } const onGridClick = (event: GridEvent) => { - if (!currentLevelId) return; + if (!currentLevelId) return - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - const y = event.position[1]; + const gridX = Math.round(event.position[0] * 2) / 2 + const gridZ = Math.round(event.position[2] * 2) / 2 + const y = event.position[1] if (!corner1Ref.current) { // First click - set corner 1 - corner1Ref.current = [gridX, y, gridZ]; + corner1Ref.current = [gridX, y, gridZ] setPreview((prev) => ({ ...prev, corner1: corner1Ref.current, - })); + })) } else { // Second click - create the roof - const roofId = commitRoofPlacement( - currentLevelId, - corner1Ref.current, - [gridX, y, gridZ] - ); + const roofId = commitRoofPlacement(currentLevelId, corner1Ref.current, [gridX, y, gridZ]) // Auto-select the newly created roof - setSelection({ selectedIds: [roofId as AnyNode["id"]] }); + setSelection({ selectedIds: [roofId as AnyNode['id']] }) // Reset state - corner1Ref.current = null; - outlineRef.current.visible = false; + corner1Ref.current = null + outlineRef.current.visible = false // Switch to select mode and deactivate tool - setMode('select'); - setTool(null); + setMode('select') + setTool(null) } - }; + } const onCancel = () => { if (corner1Ref.current) { - corner1Ref.current = null; - outlineRef.current.visible = false; - setPreview((prev) => ({ ...prev, corner1: null })); + corner1Ref.current = null + outlineRef.current.visible = false + setPreview((prev) => ({ ...prev, corner1: null })) } - }; + } // Subscribe to events - emitter.on("grid:move", onGridMove); - emitter.on("grid:click", onGridClick); - emitter.on("tool:cancel", onCancel); + emitter.on('grid:move', onGridMove) + emitter.on('grid:click', onGridClick) + emitter.on('tool:cancel', onCancel) return () => { - emitter.off("grid:move", onGridMove); - emitter.off("grid:click", onGridClick); - emitter.off("tool:cancel", onCancel); + emitter.off('grid:move', onGridMove) + emitter.off('grid:click', onGridClick) + emitter.off('tool:cancel', onCancel) // Reset state on unmount - corner1Ref.current = null; - }; - }, [currentLevelId, setTool, setSelection, setMode]); + corner1Ref.current = null + } + }, [currentLevelId, setTool, setSelection, setMode]) - const { corner1, cursorPosition, levelY } = preview; + const { corner1, cursorPosition, levelY } = preview // Calculate preview dimensions for display const previewDimensions = useMemo(() => { - if (!corner1) return null; - const length = Math.abs(cursorPosition[0] - corner1[0]); - const width = Math.abs(cursorPosition[2] - corner1[2]); - const centerX = (corner1[0] + cursorPosition[0]) / 2; - const centerZ = (corner1[2] + cursorPosition[2]) / 2; - return { length, width, centerX, centerZ }; - }, [corner1, cursorPosition]); + if (!corner1) return null + const length = Math.abs(cursorPosition[0] - corner1[0]) + const width = Math.abs(cursorPosition[2] - corner1[2]) + const centerX = (corner1[0] + cursorPosition[0]) / 2 + const centerZ = (corner1[2] + cursorPosition[2]) / 2 + return { length, width, centerX, centerZ } + }, [corner1, cursorPosition]) return ( @@ -192,34 +201,25 @@ export const RoofTool: React.FC = () => { {/* @ts-ignore */} - + {/* First corner marker */} {corner1 && ( - + - + )} {/* Cursor marker on ground */} - + - + {/* Thin preview fill when drawing */} @@ -240,5 +240,5 @@ export const RoofTool: React.FC = () => { )} - ); -}; + ) +} diff --git a/apps/editor/components/tools/shared/cursor-sphere.tsx b/apps/editor/components/tools/shared/cursor-sphere.tsx new file mode 100644 index 00000000..641edbe8 --- /dev/null +++ b/apps/editor/components/tools/shared/cursor-sphere.tsx @@ -0,0 +1,19 @@ +import { forwardRef } from 'react' +import type { Mesh } from 'three' + +interface CursorSphereProps extends Omit { + color?: string + depthWrite?: boolean +} + +export const CursorSphere = forwardRef(function CursorSphere( + { color = '#f1c066', ...props }, + ref, +) { + return ( + + + + + ) +}) diff --git a/apps/editor/components/tools/slab/slab-tool.tsx b/apps/editor/components/tools/slab/slab-tool.tsx index facdc8de..7f88f54f 100644 --- a/apps/editor/components/tools/slab/slab-tool.tsx +++ b/apps/editor/components/tools/slab/slab-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three' import { sfxEmitter } from '@/lib/sfx-bus' +import { CursorSphere } from '../shared/cursor-sphere' const Y_OFFSET = 0.02 @@ -236,10 +237,7 @@ export const SlabTool: React.FC = () => { return ( {/* Cursor */} - - - - + {/* Preview fill */} {previewShape && ( @@ -282,14 +280,7 @@ export const SlabTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => ( - - - - + ))} ) diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx index fa51d65b..72755cb0 100644 --- a/apps/editor/components/tools/wall/wall-tool.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef } from 'react' import { DoubleSide, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three' import { sfxEmitter } from '@/lib/sfx-bus' +import { CursorSphere } from '../shared/cursor-sphere' const WALL_HEIGHT = 2.5 const WALL_THICKNESS = 0.15 @@ -183,10 +184,7 @@ export const WallTool: React.FC = () => { return ( {/* Cursor indicator */} - - - - + {/* Wall preview */} diff --git a/apps/editor/components/tools/zone/zone-tool.tsx b/apps/editor/components/tools/zone/zone-tool.tsx index c9af47b9..dfc5dae7 100644 --- a/apps/editor/components/tools/zone/zone-tool.tsx +++ b/apps/editor/components/tools/zone/zone-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer"; import { useEffect, useMemo, useRef, useState } from "react"; import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from "three"; import useEditor from "@/store/use-editor"; +import { CursorSphere } from "../shared/cursor-sphere"; // Zone colors for cycling through const ZONE_COLORS = [ @@ -317,14 +318,7 @@ export const ZoneTool: React.FC = () => { return ( {/* Cursor */} - - - - + {/* Preview fill */} {previewShape && ( @@ -373,14 +367,7 @@ export const ZoneTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => isValidPoint([x, z]) ? ( - - - - + ) : null )} diff --git a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx index 7094a4d9..ee86c9a5 100644 --- a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx +++ b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx @@ -1,7 +1,7 @@ import { type CeilingNode, useRegistry } from '@pascal-app/core' import { useRef } from 'react' import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl' -import { DoubleSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu' +import { type Mesh, MeshBasicNodeMaterial } from 'three/webgpu' import { useNodeEvents } from '../../../hooks/use-node-events' import { NodeRenderer } from '../node-renderer' @@ -10,7 +10,6 @@ import { NodeRenderer } from '../node-renderer' // - Front face (looking down at ceiling from above): 30% opacity const ceilingMaterial = new MeshBasicNodeMaterial({ color: 0x999999, - side: DoubleSide, transparent: true, depthWrite: false, }) @@ -30,8 +29,8 @@ const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1. // Combine: if either X or Y is a line, show the line const gridPattern = lineX.max(lineY) -// Grid lines at 0.8 opacity, spaces at 0.1 opacity -const gridOpacity = mix(float(0.1), float(0.8), gridPattern) +// Grid lines at 0.5 opacity, spaces at 0 opacity +const gridOpacity = mix(float(0.0), float(0.5), gridPattern) // faceDirection is 1.0 for front face, -1.0 for back face // Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid From 63cf407cac7641d1bb54da89ce9fea47befb3569 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 16:13:37 +0900 Subject: [PATCH 10/11] persist viewer preferences --- packages/viewer/src/store/use-viewer.ts | 111 ++++++++++++++---------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts index d87dc117..49feaeb0 100644 --- a/packages/viewer/src/store/use-viewer.ts +++ b/packages/viewer/src/store/use-viewer.ts @@ -10,6 +10,7 @@ import type { import type { Object3D } from "three"; import { create } from "zustand"; +import { persist } from "zustand/middleware"; type SelectionPath = { buildingId: BuildingNode["id"] | null; @@ -57,62 +58,76 @@ type ViewerState = { setCameraDragging: (dragging: boolean) => void } -const useViewer = create()((set, get) => ({ - selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] }, - hoveredId: null, - setHoveredId: (id) => set({ hoveredId: id }), +const useViewer = create()( + persist( + (set) => ({ + selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] }, + hoveredId: null, + setHoveredId: (id) => set({ hoveredId: id }), - cameraMode: "perspective", - setCameraMode: (mode) => set({ cameraMode: mode }), + cameraMode: "perspective", + setCameraMode: (mode) => set({ cameraMode: mode }), - levelMode: "stacked", - setLevelMode: (mode) => set({ levelMode: mode }), + levelMode: "stacked", + setLevelMode: (mode) => set({ levelMode: mode }), - wallMode: 'cutaway', - setWallMode: (mode) => set({ wallMode: mode }), + wallMode: 'cutaway', + setWallMode: (mode) => set({ wallMode: mode }), - showScans: true, - setShowScans: (show) => set({ showScans: show }), + showScans: true, + setShowScans: (show) => set({ showScans: show }), - showGuides: true, - setShowGuides: (show) => set({ showGuides: show }), + showGuides: true, + setShowGuides: (show) => set({ showGuides: show }), - setSelection: (updates) => - set((state) => { - const newSelection = { ...state.selection, ...updates }; + setSelection: (updates) => + set((state) => { + const newSelection = { ...state.selection, ...updates }; - // Hierarchy Guard: If we change a high-level parent, reset the children - if (updates.buildingId !== undefined) { - newSelection.levelId = null; - newSelection.zoneId = null; - newSelection.selectedIds = []; - } else if (updates.levelId !== undefined) { - newSelection.zoneId = null; - newSelection.selectedIds = []; - } else if (updates.zoneId !== undefined) { - newSelection.selectedIds = []; - } + // Hierarchy Guard: If we change a high-level parent, reset the children + if (updates.buildingId !== undefined) { + newSelection.levelId = null; + newSelection.zoneId = null; + newSelection.selectedIds = []; + } else if (updates.levelId !== undefined) { + newSelection.zoneId = null; + newSelection.selectedIds = []; + } else if (updates.zoneId !== undefined) { + newSelection.selectedIds = []; + } - return { selection: newSelection }; + return { selection: newSelection }; + }), + + resetSelection: () => + set({ + selection: { + buildingId: null, + levelId: null, + zoneId: null, + selectedIds: [], + }, + }), + + outliner: { selectedObjects: [], hoveredObjects: [] }, + + exportScene: null, + setExportScene: (fn) => set({ exportScene: fn }), + + cameraDragging: false, + setCameraDragging: (dragging) => set({ cameraDragging: dragging }), }), - - resetSelection: () => - set({ - selection: { - buildingId: null, - levelId: null, - zoneId: null, - selectedIds: [], - }, - }), - - outliner: { selectedObjects: [], hoveredObjects: [] }, - - exportScene: null, - setExportScene: (fn) => set({ exportScene: fn }), - - cameraDragging: false, - setCameraDragging: (dragging) => set({ cameraDragging: dragging }), -})); + { + name: 'viewer-preferences', + partialize: (state) => ({ + cameraMode: state.cameraMode, + levelMode: state.levelMode, + wallMode: state.wallMode, + showScans: state.showScans, + showGuides: state.showGuides, + }), + }, + ), +); export default useViewer; From aee8ac5e762ad60d80ac739df16793d41966aa30 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 16:16:07 +0900 Subject: [PATCH 11/11] fix build --- apps/editor/components/tools/shared/cursor-sphere.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/editor/components/tools/shared/cursor-sphere.tsx b/apps/editor/components/tools/shared/cursor-sphere.tsx index 641edbe8..c7578fd3 100644 --- a/apps/editor/components/tools/shared/cursor-sphere.tsx +++ b/apps/editor/components/tools/shared/cursor-sphere.tsx @@ -1,13 +1,14 @@ +import type { ThreeElements } from '@react-three/fiber' import { forwardRef } from 'react' import type { Mesh } from 'three' -interface CursorSphereProps extends Omit { +interface CursorSphereProps extends Omit { color?: string depthWrite?: boolean } export const CursorSphere = forwardRef(function CursorSphere( - { color = '#f1c066', ...props }, + { color = '#f1c066', ...props }, ref, ) { return (