mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 14:44:48 +00:00
refactor: simplify 3d scene state
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { clsxm } from '@afilmory/utils'
|
||||
import { WebGLImageViewer } from '@afilmory/webgl-viewer'
|
||||
import { AnimatePresence, m } from 'motion/react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'
|
||||
import { useMediaQuery } from 'usehooks-ts'
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
useLivePhotoControls,
|
||||
useProgressiveImageState,
|
||||
useScaleIndicator,
|
||||
useThreeDSceneState,
|
||||
useWebGLLoadingState,
|
||||
} from './hooks'
|
||||
import type { ProgressiveImageProps, WebGLImageViewerRef } from './types'
|
||||
@@ -54,6 +55,7 @@ export const ProgressiveImage = ({
|
||||
|
||||
// State management
|
||||
const [state, setState] = useProgressiveImageState()
|
||||
const [threeDState, setThreeDState] = useThreeDSceneState()
|
||||
const {
|
||||
blobSrc,
|
||||
highResLoaded,
|
||||
@@ -64,19 +66,23 @@ export const ProgressiveImage = ({
|
||||
isThumbnailLoaded,
|
||||
isLivePhotoPlaying,
|
||||
} = state
|
||||
|
||||
const [isThreeDMode, setIsThreeDMode] = useState(false)
|
||||
const [isThreeDLoading, setIsThreeDLoading] = useState(false)
|
||||
const [threeDError, setThreeDError] = useState<string | null>(null)
|
||||
const [threeDLoadError, setThreeDLoadError] = useState<string | null>(null)
|
||||
const [threeDBytes, setThreeDBytes] = useState<Uint8Array | null>(null)
|
||||
const [threeDBytesForViewer, setThreeDBytesForViewer] = useState<Uint8Array | null>(null)
|
||||
const [isThreeDBytesLoading, setIsThreeDBytesLoading] = useState(false)
|
||||
const [isThreeDSceneReady, setIsThreeDSceneReady] = useState(false)
|
||||
const [webglLoadState, setWebglLoadState] = useState<'idle' | 'loading' | 'done'>('idle')
|
||||
const { isThreeDMode, threeDBytes, threeDLoadState, webglLoadState } = threeDState
|
||||
const {
|
||||
setIsThreeDMode,
|
||||
setThreeDBytes,
|
||||
setThreeDLoadState,
|
||||
setWebglLoadState,
|
||||
resetForInactive: resetThreeDForInactive,
|
||||
resetForMissingScene: resetThreeDForMissingScene,
|
||||
} = setThreeDState
|
||||
|
||||
const isActiveImage = Boolean(isCurrentImage && shouldRenderHighRes)
|
||||
const hasThreeDScene = Boolean(threeDScene && threeDScene.mode === 'sog')
|
||||
const isThreeDBytesLoading = threeDLoadState.status === 'fetching'
|
||||
const isThreeDLoading = threeDLoadState.status === 'rendering'
|
||||
const isThreeDSceneReady = threeDLoadState.status === 'ready'
|
||||
const threeDLoadError = threeDLoadState.status === 'fetchError' ? threeDLoadState.message : null
|
||||
const threeDError = threeDLoadState.status === 'renderError' ? threeDLoadState.message : null
|
||||
const isThreeDAssetReady = hasThreeDScene && Boolean(threeDBytes?.byteLength) && !threeDLoadError
|
||||
|
||||
// 判断是否有视频内容(Live Photo 或 Motion Photo)
|
||||
@@ -91,31 +97,17 @@ export const ProgressiveImage = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActiveImage) {
|
||||
setIsThreeDMode(false)
|
||||
setIsThreeDLoading(false)
|
||||
setThreeDError(null)
|
||||
setThreeDLoadError(null)
|
||||
setThreeDBytesForViewer(null)
|
||||
setIsThreeDBytesLoading(false)
|
||||
setIsThreeDSceneReady(false)
|
||||
setWebglLoadState('idle')
|
||||
resetThreeDForInactive()
|
||||
hasAutoEnteredThreeDRef.current = false
|
||||
}
|
||||
}, [isActiveImage])
|
||||
}, [isActiveImage, resetThreeDForInactive])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasThreeDScene) {
|
||||
setIsThreeDMode(false)
|
||||
setThreeDError(null)
|
||||
setThreeDLoadError(null)
|
||||
setThreeDBytes(null)
|
||||
setThreeDBytesForViewer(null)
|
||||
setIsThreeDBytesLoading(false)
|
||||
setIsThreeDSceneReady(false)
|
||||
setWebglLoadState('idle')
|
||||
resetThreeDForMissingScene()
|
||||
hasAutoEnteredThreeDRef.current = false
|
||||
}
|
||||
}, [hasThreeDScene])
|
||||
}, [hasThreeDScene, resetThreeDForMissingScene])
|
||||
|
||||
const resolvedSrc = useMemo(() => {
|
||||
if (src.startsWith('/')) {
|
||||
@@ -155,7 +147,7 @@ export const ProgressiveImage = ({
|
||||
setWebglLoadState(isLoading ? 'loading' : 'done')
|
||||
handleWebGLLoadingStateChangeBase(...args)
|
||||
},
|
||||
[handleWebGLLoadingStateChangeBase],
|
||||
[handleWebGLLoadingStateChangeBase, setWebglLoadState],
|
||||
)
|
||||
const handleThreeDLoadingStateUpdate = useCallback(
|
||||
(state: { isVisible?: boolean; loadingProgress?: number; loadedBytes?: number; totalBytes?: number }) => {
|
||||
@@ -193,30 +185,42 @@ export const ProgressiveImage = ({
|
||||
const usesWebGLViewer = !hasVideo && !shouldUseHDR && canUseWebGL
|
||||
const handleToggleThreeD = useCallback(() => {
|
||||
if (!hasThreeDScene || !isActiveImage || !canUseWebGL) return
|
||||
setThreeDError(null)
|
||||
setIsThreeDMode((prev) => {
|
||||
const next = !prev
|
||||
setThreeDLoadState((prevState) => {
|
||||
if (prevState.status === 'fetchError') return prevState
|
||||
if (!next) {
|
||||
return threeDBytes?.byteLength ? { status: 'bytesReady' } : { status: 'idle' }
|
||||
}
|
||||
return prevState.status === 'renderError' ? { status: 'bytesReady' } : prevState
|
||||
})
|
||||
if (!next) {
|
||||
setIsThreeDSceneReady(false)
|
||||
webglImageViewerRef.current?.resetView()
|
||||
domImageViewerRef.current?.resetTransform?.()
|
||||
setIsThreeDLoading(false)
|
||||
onZoomChange?.(false)
|
||||
} else {
|
||||
onZoomChange?.(true)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [hasThreeDScene, isActiveImage, onZoomChange])
|
||||
}, [hasThreeDScene, isActiveImage, canUseWebGL, onZoomChange, setIsThreeDMode, setThreeDLoadState, threeDBytes])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasThreeDScene || !isActiveImage || !canUseWebGL || !isThreeDAssetReady) return
|
||||
if (hasAutoEnteredThreeDRef.current) return
|
||||
hasAutoEnteredThreeDRef.current = true
|
||||
setThreeDError(null)
|
||||
setThreeDLoadState((prevState) => (prevState.status === 'renderError' ? { status: 'bytesReady' } : prevState))
|
||||
setIsThreeDMode(true)
|
||||
onZoomChange?.(true)
|
||||
}, [hasThreeDScene, isActiveImage, isThreeDAssetReady, onZoomChange])
|
||||
}, [
|
||||
hasThreeDScene,
|
||||
isActiveImage,
|
||||
canUseWebGL,
|
||||
isThreeDAssetReady,
|
||||
onZoomChange,
|
||||
setIsThreeDMode,
|
||||
setThreeDLoadState,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasThreeDScene || !threeDScene || !isActiveImage || !highResLoaded) return
|
||||
@@ -226,8 +230,7 @@ export const ProgressiveImage = ({
|
||||
|
||||
let cancelled = false
|
||||
const loader = imageLoaderManagerRef.current
|
||||
setIsThreeDBytesLoading(true)
|
||||
setThreeDLoadError(null)
|
||||
setThreeDLoadState({ status: 'fetching' })
|
||||
|
||||
const loadThreeD = async () => {
|
||||
try {
|
||||
@@ -239,11 +242,11 @@ export const ProgressiveImage = ({
|
||||
} catch (err) {
|
||||
if (cancelled) return
|
||||
const message = err instanceof Error ? err.message : 'Failed to load 3D scene'
|
||||
setThreeDLoadError(message)
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsThreeDBytesLoading(false)
|
||||
}
|
||||
setThreeDLoadState({ status: 'fetchError', message })
|
||||
return
|
||||
}
|
||||
if (!cancelled) {
|
||||
setThreeDLoadState({ status: 'bytesReady' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,37 +267,41 @@ export const ProgressiveImage = ({
|
||||
threeDBytes,
|
||||
threeDLoadError,
|
||||
handleThreeDLoadingStateUpdate,
|
||||
setThreeDBytes,
|
||||
setThreeDLoadState,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isThreeDMode) {
|
||||
setThreeDBytesForViewer(null)
|
||||
return
|
||||
}
|
||||
if (!threeDBytes || !threeDBytes.byteLength) return
|
||||
setThreeDBytesForViewer(threeDBytes.slice())
|
||||
}, [isThreeDMode, threeDBytes])
|
||||
|
||||
const shouldRenderThreeDScene =
|
||||
hasThreeDScene &&
|
||||
isThreeDMode &&
|
||||
isActiveImage &&
|
||||
canUseWebGL &&
|
||||
Boolean(threeDBytesForViewer?.byteLength) &&
|
||||
Boolean(threeDBytes?.byteLength) &&
|
||||
!threeDLoadError
|
||||
const shouldRenderHighResImage =
|
||||
highResLoaded && blobSrc && isActiveImage && !error && (!isThreeDMode || !isThreeDSceneReady)
|
||||
const handleThreeDLoadingChange = useCallback((loading: boolean) => {
|
||||
setIsThreeDLoading(loading)
|
||||
}, [])
|
||||
const handleThreeDError = useCallback((err: Error) => {
|
||||
setThreeDError(err?.message ?? 'unknown')
|
||||
setIsThreeDSceneReady(false)
|
||||
}, [])
|
||||
const handleThreeDLoadingChange = useCallback(
|
||||
(loading: boolean) => {
|
||||
if (loading) {
|
||||
setThreeDLoadState({ status: 'rendering' })
|
||||
return
|
||||
}
|
||||
setThreeDLoadState((prevState) => {
|
||||
if (prevState.status !== 'rendering') return prevState
|
||||
return threeDBytes?.byteLength ? { status: 'bytesReady' } : { status: 'idle' }
|
||||
})
|
||||
},
|
||||
[setThreeDLoadState, threeDBytes],
|
||||
)
|
||||
const handleThreeDError = useCallback(
|
||||
(err: Error) => {
|
||||
setThreeDLoadState({ status: 'renderError', message: err?.message ?? 'unknown' })
|
||||
},
|
||||
[setThreeDLoadState],
|
||||
)
|
||||
const handleThreeDReady = useCallback(() => {
|
||||
setThreeDError(null)
|
||||
setIsThreeDSceneReady(true)
|
||||
}, [])
|
||||
setThreeDLoadState({ status: 'ready' })
|
||||
}, [setThreeDLoadState])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -408,7 +415,7 @@ export const ProgressiveImage = ({
|
||||
!isThreeDSceneReady && 'pointer-events-none opacity-0',
|
||||
)}
|
||||
scene={threeDScene}
|
||||
sceneBytes={threeDBytesForViewer}
|
||||
sceneBytes={threeDBytes}
|
||||
imageWidth={width}
|
||||
imageHeight={height}
|
||||
loadingIndicatorRef={loadingIndicatorRef}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ImageLoaderManager } from '~/lib/image-loader-manager'
|
||||
import type { LoadingIndicatorRef } from '~/modules/inspector/LoadingIndicator'
|
||||
import type { LivePhotoVideoHandle } from '~/modules/media/LivePhotoVideo'
|
||||
|
||||
import type { ProgressiveImageState } from './types'
|
||||
import type { ProgressiveImageState, ThreeDLoadState, ThreeDSceneState, WebGLLoadState } from './types'
|
||||
import { SHOW_SCALE_INDICATOR_DURATION } from './types'
|
||||
|
||||
export const useProgressiveImageState = (): [
|
||||
@@ -59,6 +59,53 @@ export const useProgressiveImageState = (): [
|
||||
]
|
||||
}
|
||||
|
||||
export const useThreeDSceneState = (): [
|
||||
ThreeDSceneState,
|
||||
{
|
||||
setIsThreeDMode: (value: boolean | ((prev: boolean) => boolean)) => void
|
||||
setThreeDBytes: (bytes: Uint8Array | null) => void
|
||||
setThreeDLoadState: (value: ThreeDLoadState | ((prev: ThreeDLoadState) => ThreeDLoadState)) => void
|
||||
setWebglLoadState: (state: WebGLLoadState) => void
|
||||
resetForInactive: () => void
|
||||
resetForMissingScene: () => void
|
||||
},
|
||||
] => {
|
||||
const [isThreeDMode, setIsThreeDMode] = useState(false)
|
||||
const [threeDBytes, setThreeDBytes] = useState<Uint8Array | null>(null)
|
||||
const [threeDLoadState, setThreeDLoadState] = useState<ThreeDLoadState>({ status: 'idle' })
|
||||
const [webglLoadState, setWebglLoadState] = useState<WebGLLoadState>('idle')
|
||||
|
||||
const resetForInactive = useCallback(() => {
|
||||
setIsThreeDMode(false)
|
||||
setThreeDLoadState(threeDBytes?.byteLength ? { status: 'bytesReady' } : { status: 'idle' })
|
||||
setWebglLoadState('idle')
|
||||
}, [setIsThreeDMode, setThreeDLoadState, setWebglLoadState, threeDBytes])
|
||||
|
||||
const resetForMissingScene = useCallback(() => {
|
||||
setIsThreeDMode(false)
|
||||
setThreeDBytes(null)
|
||||
setThreeDLoadState({ status: 'idle' })
|
||||
setWebglLoadState('idle')
|
||||
}, [setIsThreeDMode, setThreeDBytes, setThreeDLoadState, setWebglLoadState])
|
||||
|
||||
return [
|
||||
{
|
||||
isThreeDMode,
|
||||
threeDBytes,
|
||||
threeDLoadState,
|
||||
webglLoadState,
|
||||
},
|
||||
{
|
||||
setIsThreeDMode,
|
||||
setThreeDBytes,
|
||||
setThreeDLoadState,
|
||||
setWebglLoadState,
|
||||
resetForInactive,
|
||||
resetForMissingScene,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const useImageLoader = (
|
||||
src: string,
|
||||
isCurrentImage: boolean,
|
||||
|
||||
@@ -6,6 +6,17 @@ import type { LivePhotoVideoHandle } from '../media'
|
||||
|
||||
export const SHOW_SCALE_INDICATOR_DURATION = 1000
|
||||
|
||||
export type WebGLLoadState = 'idle' | 'loading' | 'done'
|
||||
|
||||
export type ThreeDLoadState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'fetching' }
|
||||
| { status: 'fetchError'; message: string }
|
||||
| { status: 'bytesReady' }
|
||||
| { status: 'rendering' }
|
||||
| { status: 'renderError'; message: string }
|
||||
| { status: 'ready' }
|
||||
|
||||
// Video source 的 sum type:Live Photo 或 Motion Photo
|
||||
export type VideoSource =
|
||||
| { type: 'live-photo'; videoUrl: string }
|
||||
@@ -79,3 +90,10 @@ export interface ProgressiveImageState {
|
||||
isThumbnailLoaded: boolean
|
||||
isLivePhotoPlaying: boolean
|
||||
}
|
||||
|
||||
export interface ThreeDSceneState {
|
||||
isThreeDMode: boolean
|
||||
threeDBytes: Uint8Array | null
|
||||
threeDLoadState: ThreeDLoadState
|
||||
webglLoadState: WebGLLoadState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user