From 06e6ccb88fe4bab3a097cd3e1d54b322c27cedc4 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 17 Feb 2026 07:33:23 +0900 Subject: [PATCH 1/8] fix nested button --- apps/editor/features/community/components/property-grid.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/editor/features/community/components/property-grid.tsx b/apps/editor/features/community/components/property-grid.tsx index 61b58f23..8a36be8d 100644 --- a/apps/editor/features/community/components/property-grid.tsx +++ b/apps/editor/features/community/components/property-grid.tsx @@ -118,10 +118,10 @@ export function PropertyGrid({ <>
{properties.map((property) => ( -
- + ))} From 144a2bd18fbbb61c40a47309b16a11bfc18b00bb Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 17 Feb 2026 08:52:58 +0900 Subject: [PATCH 2/8] camera controls enhancement --- .../viewer/[id]/viewer-camera-controls.tsx | 22 ++++- .../editor/custom-camera-controls.tsx | 93 ++++++++++++++++++- apps/editor/hooks/use-grid-events.ts | 9 +- .../components/viewer/selection-manager.tsx | 49 ++++++++-- packages/viewer/src/hooks/use-node-events.ts | 14 ++- packages/viewer/src/store/use-viewer.ts | 6 ++ 6 files changed, 171 insertions(+), 22 deletions(-) diff --git a/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx b/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx index e43bd28e..ab59152b 100644 --- a/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx +++ b/apps/editor/app/viewer/[id]/viewer-camera-controls.tsx @@ -3,7 +3,7 @@ import { sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { CameraControls, CameraControlsImpl } from '@react-three/drei' -import { useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { Box3, Vector3 } from 'three' const tempBox = new Box3() @@ -28,7 +28,7 @@ export const ViewerCameraControls = () => { : CameraControlsImpl.ACTION.DOLLY return { - left: CameraControlsImpl.ACTION.NONE, + left: CameraControlsImpl.ACTION.SCREEN_PAN, middle: CameraControlsImpl.ACTION.SCREEN_PAN, right: CameraControlsImpl.ACTION.ROTATE, wheel: wheelAction, @@ -59,7 +59,7 @@ export const ViewerCameraControls = () => { target[0], target[1], target[2], - true + true, ) return } @@ -81,7 +81,7 @@ export const ViewerCameraControls = () => { const cameraPos = new Vector3( tempCenter.x + distance * 0.7, tempCenter.y + distance * 0.5, - tempCenter.z + distance * 0.7 + tempCenter.z + distance * 0.7, ) controls.current.setLookAt( @@ -91,10 +91,18 @@ export const ViewerCameraControls = () => { tempCenter.x, tempCenter.y, tempCenter.z, - true + true, ) }, [targetNodeId, nodes]) + const onTransitionStart = useCallback(() => { + useViewer.getState().setCameraDragging(true) + }, []) + + const onRest = useCallback(() => { + useViewer.getState().setCameraDragging(false) + }, []) + return ( { maxPolarAngle={Math.PI / 2 - 0.1} minPolarAngle={0} mouseButtons={mouseButtons} + onTransitionStart={onTransitionStart} + onRest={onRest} + restThreshold={0.01} + /> ) } diff --git a/apps/editor/components/editor/custom-camera-controls.tsx b/apps/editor/components/editor/custom-camera-controls.tsx index b6039a52..e96e916a 100644 --- a/apps/editor/components/editor/custom-camera-controls.tsx +++ b/apps/editor/components/editor/custom-camera-controls.tsx @@ -3,7 +3,7 @@ import { type CameraControlEvent, emitter, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { CameraControls, CameraControlsImpl } from '@react-three/drei' -import { useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { Vector3 } from 'three' const currentTarget = new Vector3() @@ -51,6 +51,86 @@ export const CustomCameraControls = () => { } }, [cameraMode]) + useEffect(() => { + console.log('ohla') + const keyState = { + shiftRight: false, + shiftLeft: false, + controlRight: false, + controlLeft: false, + space: false, + } + + const updateConfig = () => { + if (!controls.current) return + + const shift = keyState.shiftRight || keyState.shiftLeft + const control = keyState.controlRight || keyState.controlLeft + const space = keyState.space + + const wheelAction = + cameraMode === 'orthographic' + ? CameraControlsImpl.ACTION.ZOOM + : CameraControlsImpl.ACTION.DOLLY + controls.current.mouseButtons.wheel = wheelAction + controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE + controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN + controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE + if (space) { + controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN + } + } + + const onKeyDown = (event: KeyboardEvent) => { + if (event.code === 'Space') { + keyState.space = true + document.body.style.cursor = 'grab' + } + if (event.code === 'ShiftRight') { + keyState.shiftRight = true + } + if (event.code === 'ShiftLeft') { + keyState.shiftLeft = true + } + if (event.code === 'ControlRight') { + keyState.controlRight = true + } + if (event.code === 'ControlLeft') { + keyState.controlLeft = true + } + updateConfig() + } + + const onKeyUp = (event: KeyboardEvent) => { + if (event.code === 'Space') { + keyState.space = false + document.body.style.cursor = '' + } + if (event.code === 'ShiftRight') { + keyState.shiftRight = false + } + if (event.code === 'ShiftLeft') { + keyState.shiftLeft = false + } + if (event.code === 'ControlRight') { + keyState.controlRight = false + } + if (event.code === 'ControlLeft') { + keyState.controlLeft = false + } + updateConfig() + } + + document.addEventListener('keydown', onKeyDown) + document.addEventListener('keyup', onKeyUp) + updateConfig() + + return () => { + document.removeEventListener('keydown', onKeyDown) + document.removeEventListener('keyup', onKeyUp) + } + }, [cameraMode]) + useEffect(() => { const handleNodeCapture = ({ nodeId }: CameraControlEvent) => { if (!controls.current) return @@ -139,6 +219,14 @@ export const CustomCameraControls = () => { } }, []) + const onTransitionStart = useCallback(() => { + useViewer.getState().setCameraDragging(true) + }, []) + + const onRest = useCallback(() => { + useViewer.getState().setCameraDragging(false) + }, []) + return ( { minPolarAngle={0} ref={controls} mouseButtons={mouseButtons} + onTransitionStart={onTransitionStart} + onRest={onRest} + restThreshold={0.01} /> ) } diff --git a/apps/editor/hooks/use-grid-events.ts b/apps/editor/hooks/use-grid-events.ts index 2a48514e..ddee9065 100644 --- a/apps/editor/hooks/use-grid-events.ts +++ b/apps/editor/hooks/use-grid-events.ts @@ -1,4 +1,5 @@ import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' import { useThree } from '@react-three/fiber' import { useEffect, useRef } from 'react' import { Plane, Raycaster, Vector2, Vector3 } from 'three' @@ -53,29 +54,35 @@ export function useGridEvents(gridY: number) { } const handlePointerDown = (e: PointerEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('pointerdown', e) } const handlePointerUp = (e: PointerEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('pointerup', e) } const handleClick = (e: PointerEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('click', e) } const handlePointerMove = (e: PointerEvent) => { + if (useViewer.getState().cameraDragging) return emit('move', e) } const handleDoubleClick = (e: MouseEvent) => { + if (useViewer.getState().cameraDragging) return emit('double-click', e) } const handleContextMenu = (e: MouseEvent) => { + if (useViewer.getState().cameraDragging) return emit('context-menu', e) } @@ -95,5 +102,5 @@ export function useGridEvents(gridY: number) { canvas.removeEventListener('dblclick', handleDoubleClick) canvas.removeEventListener('contextmenu', handleContextMenu) } - }, [camera, gl, gridY]) + }, [camera, gl]) } diff --git a/packages/viewer/src/components/viewer/selection-manager.tsx b/packages/viewer/src/components/viewer/selection-manager.tsx index 2f45a870..433a0657 100644 --- a/packages/viewer/src/components/viewer/selection-manager.tsx +++ b/packages/viewer/src/components/viewer/selection-manager.tsx @@ -3,15 +3,15 @@ import { type AnyNode, type BuildingNode, + emitter, type ItemNode, type LevelNode, type NodeEvent, - type WallNode, - type ZoneNode, - emitter, pointInPolygon, sceneRegistry, useScene, + type WallNode, + type ZoneNode, } from '@pascal-app/core' import { useThree } from '@react-three/fiber' import { useEffect, useRef } from 'react' @@ -23,14 +23,23 @@ const tempWorldPos = new Vector3() // Tolerance for edge detection (in meters) const EDGE_TOLERANCE = 0.5 -type SelectableNodeType = 'building' | 'level' | 'zone' | 'wall' | 'item' | 'slab' | 'ceiling' | 'roof' +type SelectableNodeType = + | 'building' + | 'level' + | 'zone' + | 'wall' + | 'item' + | 'slab' + | 'ceiling' + | 'roof' // Expand polygon outward by a small amount to include items on edges const expandPolygon = (polygon: [number, number][], tolerance: number): [number, number][] => { if (polygon.length < 3) return polygon // Calculate centroid - let cx = 0, cz = 0 + let cx = 0, + cz = 0 for (const [x, z] of polygon) { cx += x cz += z @@ -50,7 +59,11 @@ const expandPolygon = (polygon: [number, number][], tolerance: number): [number, } // Check if point is in polygon with tolerance for edges -const pointInPolygonWithTolerance = (x: number, z: number, polygon: [number, number][]): boolean => { +const pointInPolygonWithTolerance = ( + x: number, + z: number, + polygon: [number, number][], +): boolean => { // First try exact check if (pointInPolygon(x, z, polygon)) return true // Then try with expanded polygon for edge tolerance @@ -186,7 +199,9 @@ const getStrategy = (): SelectionStrategy | null => { const { selectedIds } = useViewer.getState().selection // Toggle selection - if already selected, deselect; otherwise select if (selectedIds.includes(node.id)) { - useViewer.getState().setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) }) + useViewer + .getState() + .setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) }) } else { useViewer.getState().setSelection({ selectedIds: [node.id] }) } @@ -244,7 +259,16 @@ export const SelectionManager = () => { } // Subscribe to all node types - const allTypes: SelectableNodeType[] = ['building', 'level', 'zone', 'wall', 'item', 'slab', 'ceiling', 'roof'] + const allTypes: SelectableNodeType[] = [ + 'building', + 'level', + 'zone', + 'wall', + 'item', + 'slab', + 'ceiling', + 'roof', + ] for (const type of allTypes) { emitter.on(`${type}:enter`, onEnter) emitter.on(`${type}:leave`, onLeave) @@ -258,7 +282,7 @@ export const SelectionManager = () => { emitter.off(`${type}:click`, onClick) } } - }, [selection]) + }, []) return ( <> @@ -268,12 +292,17 @@ export const SelectionManager = () => { ) } -const PointerMissedHandler = ({ clickHandledRef }: { clickHandledRef: React.MutableRefObject }) => { +const PointerMissedHandler = ({ + clickHandledRef, +}: { + clickHandledRef: React.MutableRefObject +}) => { const gl = useThree((s) => s.gl) useEffect(() => { const handleClick = (event: MouseEvent) => { // Only handle left clicks + if (useViewer.getState().cameraDragging) return if (event.button !== 0) return // Use requestAnimationFrame to check after R3F event handlers diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index c350edb7..41d4a453 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -21,6 +21,7 @@ import { type ZoneNode, } from '@pascal-app/core' import type { ThreeEvent } from '@react-three/fiber' +import useViewer from '../store/use-viewer'; type NodeConfig = { site: { node: SiteNode; event: SiteEvent } @@ -54,21 +55,24 @@ export function useNodeEvents(node: NodeConfig[T]['node'], t return { onPointerDown: (e: ThreeEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('pointerdown', e) }, onPointerUp: (e: ThreeEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('pointerup', e) }, onClick: (e: ThreeEvent) => { + if (useViewer.getState().cameraDragging) return if (e.button !== 0) return emit('click', e) }, - onPointerEnter: (e: ThreeEvent) => emit('enter', e), - onPointerLeave: (e: ThreeEvent) => emit('leave', e), - onPointerMove: (e: ThreeEvent) => emit('move', e), - onDoubleClick: (e: ThreeEvent) => emit('double-click', e), - onContextMenu: (e: ThreeEvent) => emit('context-menu', e), + onPointerEnter: (e: ThreeEvent) => {if (useViewer.getState().cameraDragging) return; emit('enter', e)}, + onPointerLeave: (e: ThreeEvent) => {if (useViewer.getState().cameraDragging) return; emit('leave', e)}, + onPointerMove: (e: ThreeEvent) => {if (useViewer.getState().cameraDragging) return; emit('move', e)}, + onDoubleClick: (e: ThreeEvent) => {if (useViewer.getState().cameraDragging) return; emit('double-click', e)}, + onContextMenu: (e: ThreeEvent) => {if (useViewer.getState().cameraDragging) return; emit('context-menu', e)}, } } diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts index b772e157..d87dc117 100644 --- a/packages/viewer/src/store/use-viewer.ts +++ b/packages/viewer/src/store/use-viewer.ts @@ -52,6 +52,9 @@ type ViewerState = { // Export functionality exportScene: (() => Promise) | null setExportScene: (fn: (() => Promise) | null) => void + + cameraDragging: boolean + setCameraDragging: (dragging: boolean) => void } const useViewer = create()((set, get) => ({ @@ -107,6 +110,9 @@ const useViewer = create()((set, get) => ({ exportScene: null, setExportScene: (fn) => set({ exportScene: fn }), + + cameraDragging: false, + setCameraDragging: (dragging) => set({ cameraDragging: dragging }), })); export default useViewer; From 078b2500a3c9c3a12b0ab49f9c66f43458837c1c Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 17 Feb 2026 12:07:32 +0900 Subject: [PATCH 3/8] handle thumbnails (manual) --- apps/editor/app/viewer/[id]/page.tsx | 7 +- .../app/viewer/[id]/thumbnail-generator.tsx | 122 +++++++++ .../editor/custom-camera-controls.tsx | 1 - apps/editor/components/editor/index.tsx | 2 + .../sidebar/panels/settings-panel/index.tsx | 43 +++- .../community/lib/properties/actions.ts | 115 +++++++++ .../community/lib/properties/hooks.ts | 234 ------------------ .../community/lib/properties/store.ts | 40 +-- packages/core/src/events/bus.ts | 5 + 9 files changed, 296 insertions(+), 273 deletions(-) create mode 100644 apps/editor/app/viewer/[id]/thumbnail-generator.tsx delete mode 100644 apps/editor/features/community/lib/properties/hooks.ts diff --git a/apps/editor/app/viewer/[id]/page.tsx b/apps/editor/app/viewer/[id]/page.tsx index a44851c9..ed4266ea 100644 --- a/apps/editor/app/viewer/[id]/page.tsx +++ b/apps/editor/app/viewer/[id]/page.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from 'react' import { ViewerCameraControls } from './viewer-camera-controls' import { ViewerOverlay } from './viewer-overlay' import { ViewerZoneSystem } from './viewer-zone-system' +import { ThumbnailGenerator } from './thumbnail-generator' import { getPropertyModelPublic, incrementPropertyViews } from '@/features/community/lib/properties/actions' export default function ViewerPage() { @@ -14,6 +15,7 @@ export default function ViewerPage() { const id = params.id as string const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [propertyId, setPropertyId] = useState(null) const setScene = useScene((state) => state.setScene) useEffect(() => { @@ -35,7 +37,8 @@ export default function ViewerPage() { const result = await getPropertyModelPublic(id) if (result.success && result.data) { - const { model } = result.data + const { property, model } = result.data + setPropertyId(property.id) if (model?.scene_graph) { const { nodes, rootNodeIds } = model.scene_graph @@ -84,6 +87,8 @@ export default function ViewerPage() { {/* Custom Zone System */} + {/* Thumbnail Generator */} + ) diff --git a/apps/editor/app/viewer/[id]/thumbnail-generator.tsx b/apps/editor/app/viewer/[id]/thumbnail-generator.tsx new file mode 100644 index 00000000..f9909caf --- /dev/null +++ b/apps/editor/app/viewer/[id]/thumbnail-generator.tsx @@ -0,0 +1,122 @@ +'use client' + +import { emitter } from '@pascal-app/core' +import { useThree } from '@react-three/fiber' +import { useEffect, useRef } from 'react' +import * as THREE from 'three' +import { uploadPropertyThumbnail } from '@/features/community/lib/properties/actions' + +const THUMBNAIL_WIDTH = 1920 +const THUMBNAIL_HEIGHT = 1080 + +interface ThumbnailGeneratorProps { + propertyId?: string +} + +export const ThumbnailGenerator = ({ propertyId: propPropertyId }: ThumbnailGeneratorProps) => { + const gl = useThree((state) => state.gl) + const scene = useThree((state) => state.scene) + const camera = useThree((state) => state.camera) + const isGenerating = useRef(false) + + // Use prop propertyId (from URL) + const fallbackPropertyId = propPropertyId + + useEffect(() => { + const handleGenerateThumbnail = async (event: { propertyId: string }) => { + if (isGenerating.current) { + console.log('⏸️ Thumbnail generation already in progress') + return + } + + // Prioritize prop propertyId over event propertyId (URL has priority over session) + const propertyId = fallbackPropertyId || event.propertyId + + if (!propertyId) { + console.error('❌ No property ID provided') + return + } + + isGenerating.current = true + console.log('πŸ“Έ Generating thumbnail for property:', propertyId) + console.log('πŸ“ Property ID from URL/prop:', fallbackPropertyId) + console.log('πŸ“ Property ID from event:', event.propertyId) + console.log('βœ… Using property ID:', propertyId, fallbackPropertyId ? '(from URL)' : '(from event)') + + try { + // Save current renderer state + const currentSize = gl.getSize(new THREE.Vector2()) + const currentPixelRatio = gl.getPixelRatio() + + // Temporarily resize renderer to thumbnail size + gl.setPixelRatio(1) + gl.setSize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) + + // Update camera aspect ratio if it's a perspective camera + if (camera instanceof THREE.PerspectiveCamera) { + camera.aspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT + camera.updateProjectionMatrix() + } + + // Render the scene + gl.render(scene, camera) + + // Wait a frame to ensure render is complete + await new Promise((resolve) => requestAnimationFrame(resolve)) + + // Capture canvas as blob + const canvas = gl.domElement + canvas.toBlob(async (blob) => { + if (blob) { + // Upload to Supabase Storage + console.log('☁️ Uploading thumbnail to storage...') + const result = await uploadPropertyThumbnail(propertyId, blob) + + if (result.success) { + console.log('βœ… Thumbnail uploaded successfully!') + console.log('πŸ”— URL:', result.data.thumbnail_url) + } else { + console.error('❌ Failed to upload thumbnail:', result.error) + } + } else { + console.error('❌ Failed to create blob from canvas') + } + + // Restore renderer size and camera + gl.setPixelRatio(currentPixelRatio) + gl.setSize(currentSize.x, currentSize.y) + + if (camera instanceof THREE.PerspectiveCamera) { + camera.aspect = currentSize.x / currentSize.y + camera.updateProjectionMatrix() + } + + isGenerating.current = false + }, 'image/png') + } catch (error) { + console.error('❌ Failed to generate thumbnail:', error) + + // Make sure to restore size even on error + const currentSize = gl.getSize(new THREE.Vector2()) + const currentPixelRatio = gl.getPixelRatio() + gl.setPixelRatio(currentPixelRatio) + gl.setSize(currentSize.x, currentSize.y) + + if (camera instanceof THREE.PerspectiveCamera) { + camera.aspect = currentSize.x / currentSize.y + camera.updateProjectionMatrix() + } + + isGenerating.current = false + } + } + + emitter.on('camera-controls:generate-thumbnail', handleGenerateThumbnail) + + return () => { + emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail) + } + }, [gl, scene, camera, fallbackPropertyId]) + + return null +} diff --git a/apps/editor/components/editor/custom-camera-controls.tsx b/apps/editor/components/editor/custom-camera-controls.tsx index e96e916a..07e717d9 100644 --- a/apps/editor/components/editor/custom-camera-controls.tsx +++ b/apps/editor/components/editor/custom-camera-controls.tsx @@ -52,7 +52,6 @@ export const CustomCameraControls = () => { }, [cameraMode]) useEffect(() => { - console.log('ohla') const keyState = { shiftRight: false, shiftLeft: false, diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 680722c3..62202521 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -20,6 +20,7 @@ import { ExportManager } from './export-manager' import { Grid } from './grid' import { SelectionManager } from './selection-manager' import { initSFXBus } from '@/lib/sfx-bus' +import { ThumbnailGenerator } from '@/app/viewer/[id]/thumbnail-generator' // Load default scene initially (will be replaced when property loads) useScene.getState().loadScene() @@ -73,6 +74,7 @@ export default function Editor({ propertyId }: EditorProps) { + ) diff --git a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx index 7009d558..f0e80906 100644 --- a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx @@ -1,10 +1,11 @@ -import { useScene } from "@pascal-app/core"; +import { emitter, useScene } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { Download, Save, Trash2, Upload } from "lucide-react"; -import { useRef } from "react"; +import { Camera, Download, Save, Trash2, Upload } from "lucide-react"; +import { useRef, useState } from "react"; import { Button } from "@/components/ui/primitives/button"; import useEditor from "@/store/use-editor"; import { AudioSettingsDialog } from "./audio-settings-dialog"; +import { usePropertyStore } from "@/features/community/lib/properties/store"; export function SettingsPanel() { const fileInputRef = useRef(null); @@ -15,6 +16,11 @@ export function SettingsPanel() { const resetSelection = useViewer((state) => state.resetSelection); const exportScene = useViewer((state) => state.exportScene); const setPhase = useEditor((state) => state.setPhase); + const activeProperty = usePropertyStore((state) => state.activeProperty); + const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false); + + const propertyId = activeProperty?.id; + const isLocalProperty = false; // Store only contains cloud properties const handleExport = async () => { if (exportScene) { @@ -64,6 +70,19 @@ export function SettingsPanel() { setPhase("site"); }; + const handleGenerateThumbnail = () => { + if (!propertyId) { + console.error('❌ No property ID found'); + return; + } + console.log('🎯 Generate thumbnail clicked for property:', propertyId); + setIsGeneratingThumbnail(true); + emitter.emit('camera-controls:generate-thumbnail', { propertyId }); + console.log('πŸ“€ Event emitted with property ID:', propertyId); + // Reset loading state after a delay (thumbnail generation is async) + setTimeout(() => setIsGeneratingThumbnail(false), 3000); + }; + return (
{/* Export Section */} @@ -81,6 +100,24 @@ export function SettingsPanel() {
+ {/* Thumbnail Section (only for cloud properties) */} + {propertyId && !isLocalProperty && ( +
+ + +
+ )} + {/* Save/Load Section */}