handle thumbnails (manual)
This commit is contained in:
@@ -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<string | null>(null)
|
||||
const [propertyId, setPropertyId] = useState<string | null>(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() {
|
||||
<ViewerCameraControls />
|
||||
{/* Custom Zone System */}
|
||||
<ViewerZoneSystem />
|
||||
{/* Thumbnail Generator */}
|
||||
<ThumbnailGenerator propertyId={propertyId || undefined} />
|
||||
</Viewer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user