UNPKG

@mcmhomes/panorama-viewer

Version:
199 lines (159 loc) 9.34 kB
/*eslint-disable react-compiler/react-compiler*/ import React from 'react'; import {Canvas} from '@react-three/fiber'; import {PerspectiveCamera} from '@react-three/drei'; import {PanoramaControls} from '../gameplay/PanoramaControls.jsx'; import {PanoramaRendererTexturePreloader} from './PanoramaRendererTexturePreloader.jsx'; import {memo, useCallback, useFadeoutAnimation, useMemo, useRef, useState} from '../utils/PanoramaUtilsReact.jsx'; import {getSelectedVariationIndexesBySku, getTexturePathsToRender} from '../utils/PanoramaVariationParsingUtils.jsx'; import {contains, each, find, findIndex, FLOAT_LAX, FLOAT_LAX_ANY, ISSET, mapToArray, setTimeoutRemovable, STRING, uniqueId} from '../utils/PanoramaUtils.jsx'; const FADE_BETWEEN_LOCATIONS = true; const FADEOUT_DELAY_MS = 200; const FADEOUT_DURATION_MS = 800; const FADEOUT_DURATION_DECAY_FACTOR = Math.pow(0.01, 1 / (FADEOUT_DURATION_MS / 1000)); const FADEOUT_DURATION_LINEAR = false; export const PanoramaRenderer = memo((props) => { const {getErrorWidget, getLoadingWidget} = props; const [renderId, setRenderId] = useState(uniqueId()); const [initialLoading, setInitialLoading] = useState(true); const layersRef = useRef([]); // noinspection com.intellij.reactbuddy.ExhaustiveDepsInspection useMemo(() => { if(!FADE_BETWEEN_LOCATIONS && (layersRef.current.length > 0)) { return; } const key = uniqueId(); const setLoading = (isLoading) => { if(isLoading) { return; } const layer = find(layersRef.current, layer => (layer.key === key)); if(layer && layer.props.loading) { layer.props.loading = false; setRenderId(uniqueId()); setInitialLoading(false); setTimeoutRemovable(() => { const index = findIndex(layersRef.current, layer => (layer.key === key)); if(index > 0) { layersRef.current.splice(0, index); setRenderId(uniqueId()); } }, FADEOUT_DELAY_MS + FADEOUT_DURATION_MS + 200); } }; layersRef.current.push({key:key, props:{loading:true, setLoading}, givenProps:props}); // changes made to the other fields within props should not be handled here, instead, they will be handled in the useMemo below //eslint-disable-next-line react-hooks/exhaustive-deps }, [props?.homeUrl, props?.locationId]); // noinspection com.intellij.reactbuddy.ExhaustiveDepsInspection useMemo(() => { each(layersRef.current, layer => { if(!FADE_BETWEEN_LOCATIONS || ((layer.givenProps.homeUrl === props?.homeUrl) && (layer.givenProps.locationId === props?.locationId))) { layer.givenProps = props; } }); // this is the correct way of handling changes made to props }, [props]); return (<> {!!initialLoading && getLoadingWidget()} <div style={{position:'relative', width:'100%', height:'100%', overflow:'hidden', ...(!initialLoading ? {} : {width:'1px', height:'1px', opacity:'0'})}}> {mapToArray(layersRef.current, layer => ( <PanoramaRendererAtLocation {...layer.givenProps} {...layer.props} key={layer.key}/> ))} </div> </>); }); const PanoramaRendererAtLocation = memo(({loading, setLoading, homeId, host, homeUrl, variations, skus, styleId:givenStyleId, locationId, basisTranscoderPath, minFov, maxFov, calculateFov, onFovChanged, initialCameraRotation:givenInitialCameraRotation, onCameraRotationChanged:givenOnCameraRotationChanged, lookSpeed, lookSpeedX, lookSpeedY, zoomSpeed, getErrorWidget, getLoadingWidget}) => { const [movedCamera, setMovedCamera] = useState(false); const {opacity:fadeoutOpacity} = useFadeoutAnimation({visible:loading, delay:FADEOUT_DELAY_MS, duration:FADEOUT_DURATION_LINEAR ? FADEOUT_DURATION_MS : null, decayFactor:FADEOUT_DURATION_LINEAR ? null : FADEOUT_DURATION_DECAY_FACTOR}); const opacity = (1 - (fadeoutOpacity / (movedCamera ? 3 : 1))); const controlsMultiplier = (opacity < 0.8) ? 0 : 1; const opacityRef = useRef(); opacityRef.current = opacity; const [error, setError] = useState(null); let styleId = givenStyleId; const styles = variations?.styles; const locations = useMemo(() => styleId ? variations?.locations?.filter(location => contains(location?.supportedStyleIds, styleId)) : variations?.locations, [variations?.locations, styleId]); const locationIndex = useMemo(() => locationId ? findIndex(locations, location => (location?.locationId === locationId)) : 0, [locations, locationId]); if(!styleId) { styleId = STRING(locations?.[locationIndex]?.supportedStyleIds?.[0]); } const styleIndex = useMemo(() => styleId ? findIndex(styles, style => (style?.styleId === styleId)) : 0, [styles, styleId]); const variationGroups = styles?.[styleIndex]?.variationGroups; const locationVariationGroups = locations?.[locationIndex]?.variationGroups; const selectedVariationIndexes = useMemo(() => getSelectedVariationIndexesBySku(variationGroups, skus), [variationGroups, skus]); const src = useMemo(() => getTexturePathsToRender(variationGroups, selectedVariationIndexes, locationVariationGroups, styleIndex, locationIndex, homeUrl), [variationGroups, selectedVariationIndexes, locationVariationGroups, styleIndex, locationIndex, homeUrl]); const initialCameraRotation_locationDesiredRotation = locations?.[locationIndex]?.desiredRotation; const initialCameraRotation_locationRecommendedRotation = locations?.[locationIndex]?.recommendedRotation; const initialCameraRotation = useMemo(() => { if(ISSET(givenInitialCameraRotation?.yaw) && ISSET(givenInitialCameraRotation?.pitch)) { return {yaw:FLOAT_LAX(givenInitialCameraRotation?.yaw), pitch:FLOAT_LAX(givenInitialCameraRotation?.pitch)}; } if(ISSET(initialCameraRotation_locationDesiredRotation)) { return {yaw:FLOAT_LAX(initialCameraRotation_locationDesiredRotation), pitch:0}; } return {yaw:FLOAT_LAX(initialCameraRotation_locationRecommendedRotation?.yaw), pitch:FLOAT_LAX(initialCameraRotation_locationRecommendedRotation?.pitch)}; }, [givenInitialCameraRotation?.yaw, givenInitialCameraRotation?.pitch, initialCameraRotation_locationDesiredRotation, initialCameraRotation_locationRecommendedRotation?.yaw, initialCameraRotation_locationRecommendedRotation?.pitch]); const onCameraRotationChanged = useCallback(newRotation => { if(opacityRef.current >= 0.8) { setMovedCamera(true); } givenOnCameraRotationChanged?.(newRotation); }, [givenOnCameraRotationChanged]); const errorComponent = useMemo(() => { if(error) { return getErrorWidget(error); } if(!styles || !locations) { return getErrorWidget({canRetry:false, id:'could-not-connect-to-home', message:'Couldn\'t connect to home: ' + homeId, reason:'the home data isn\'t compatible with our frontend, it doesn\'t contain the information that should be in there', data:{homeId, host, homeUrl, variations}}); } if(styleId && locationId && (!ISSET(styleIndex) || !(styleIndex in styles) || !ISSET(locationIndex) || !(locationIndex in locations))) { return getErrorWidget({canRetry:false, id:'invalid-location-and-style-id', message:'Invalid location and style ID: ' + locationId + ' and ' + styleId, reason:'the location and style ID combination doesn\'t exist in the home', data:{homeId, host, homeUrl, variations, styleId, locationId}}); } if(!ISSET(styleIndex) || !(styleIndex in styles)) { return getErrorWidget({canRetry:false, id:'invalid-style-id', message:'Invalid style ID: ' + styleId, reason:'the style ID doesn\'t exist in the home', data:{homeId, host, homeUrl, variations, styleId}}); } if(!ISSET(locationIndex) || !(locationIndex in locations)) { return getErrorWidget({canRetry:false, id:'invalid-location-id', message:'Invalid location ID: ' + locationId, reason:'the location ID doesn\'t exist in the home', data:{homeId, host, homeUrl, variations, locationId}}); } if(!variationGroups || !locationVariationGroups) { return getErrorWidget({canRetry:false, id:'could-not-connect-to-home', message:'Couldn\'t connect to home: ' + homeId, reason:'the home data isn\'t compatible with our frontend, it doesn\'t contain the information that should be in there', data:{homeId, host, homeUrl, variations}}); } }, [error, styles, locations, styleId, locationId, styleIndex, locationIndex, variationGroups, locationVariationGroups, homeId, host, homeUrl, variations, getErrorWidget]); return (<> <div style={{position:'absolute', width:'100%', height:'100%', overflow:'hidden', ...(!loading ? {opacity} : {width:'1px', height:'1px', opacity:'0'})}}> {errorComponent || ( <Canvas flat={true} linear={true} shadows={false} frameloop="demand" gl={{precision:'highp', antialias:false, depth:false, stencil:false}}> <PerspectiveCamera makeDefault position={[0, 0, 0]}/> <PanoramaControls minFov={minFov} maxFov={maxFov} calculateFov={calculateFov} onFovChanged={onFovChanged} initialCameraRotation={initialCameraRotation} onCameraRotationChanged={onCameraRotationChanged} lookSpeed={FLOAT_LAX_ANY(lookSpeed, 1) * controlsMultiplier} lookSpeedX={lookSpeedX} lookSpeedY={lookSpeedY} zoomSpeed={FLOAT_LAX_ANY(zoomSpeed, 1) * controlsMultiplier}/> <PanoramaRendererTexturePreloader src={src} homeId={homeId} host={host} homeUrl={homeUrl} basisTranscoderPath={basisTranscoderPath} setLoading={setLoading} setError={setError}/> </Canvas> )} </div> </>); });