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 */}