@mcmhomes/panorama-viewer
Version:
Provides React components to render panoramas.
199 lines (159 loc) • 9.34 kB
JSX
/*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>
</>);
});