diff --git a/src/App.tsx b/src/App.tsx index 5ac6095..08e976a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import LogoutPage from './components/pages/LogoutPage' import RegisterPage from './components/pages/RegisterPage' import Chart from './components/pages/Chart' import { BrowserRouter, Routes, Route } from 'react-router-dom' -import './App.css' import { observer } from 'mobx-react' import { ThemeProvider, CssBaseline } from '@mui/material' import appTheme from './Theme' @@ -21,8 +20,9 @@ import '@aws-amplify/ui-react/styles.css'; import { useMediaQuery } from '@mui/material'; import { useMediaQuery as useResponsiveQuery } from 'react-responsive'; import screenfull from 'screenfull'; -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import './App.css' import awsconfig from './aws-exports'; Amplify.configure(awsconfig); @@ -37,6 +37,7 @@ function App({ signOut, user }: WithAuthenticatorProps) { const isPortrait = useDeviceOrientation(); const isSmallScreen = useMediaQuery('(max-width:600px)'); const elementRef = useRef(null); + const [ displayAppBar, setDisplayAppBar ] = useState('inherit'); const toggleFullscreen = () => { if (screenfull.isEnabled) { @@ -53,6 +54,19 @@ function App({ signOut, user }: WithAuthenticatorProps) { alert(t('app.switch_landscape')); } }, [isSmallScreen, isPortrait, t]); + + React.useEffect(() => { + // Parse URL parameters + const urlParams = new URLSearchParams(window.location.search); + const cssParam = urlParams.get('css'); // Assuming 'css' is the parameter name + + // Set gui mode if parameter is present. + if (cssParam === 'gui') { + viewerState.setIsGuiMode(true) + setDisplayAppBar('none') + } + }, []); + // On file system we'll have a folder per model containing cached/versioned gltf, possibly .osim file, data files, display // preferences // urls could be something like: @@ -71,7 +85,9 @@ function App({ signOut, user }: WithAuthenticatorProps) {
- +
+ +
} /> @@ -84,10 +100,6 @@ function App({ signOut, user }: WithAuthenticatorProps) { path="/viewer/:urlParam?" element={} /> - } - /> } diff --git a/src/backend/README_1.md b/src/backend/README_1.md deleted file mode 100644 index b2e4949..0000000 --- a/src/backend/README_1.md +++ /dev/null @@ -1,59 +0,0 @@ -*Requisites:** Conda and python installed. - -1. Create environment using the `environment.yml` file: - - `conda env create -f environment.yml` - -2. Activate environment: - - `conda activate opensim-viewer-bend` - -3. Start server: - - `python manage.py runserver` - -### Instructions for database migration - - 1. Create migration files: - - `python manage.py makemigrations` - - 2. Migrate the database (warning: data can be lost) - - `python manage.py migrate` - -### Instructions for recreating ERD diagram - -Instructions in this [Link](https://www.wplogout.com/export-database-diagrams-erd-from-django/). - -### Instructions for localization - -Instructions in this [Link](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/). - -Inside of backend app folder: - -1. Create files for a language: - - `django-admin makemessages -l ` - -2. Compile messages: - - `django-admin compilemessages` - -### Instruction for testing - -- Execute all tests: - - `python manage.py test --verbosity=0` - - -General -------- -- This folder contains scripts to convert OpenSim based data files into gltf format -- We use third party library pygltflib to manipulate the gltf structure thus avoiding low level json file manipulation and encoding/decoding whenever possible. - -Description of specific python files: -------------------------------------- -- openSimData2Gltf.py: gneric utilities that take columns of data and time stamps and create the corresponding Accessors in the passed in GLTF2 structure -- convert{xxx}2Gltf.py: Utilities for converting files with extension {xxx} to gltf, the convention is to produce a file with the same name but with different extension, unless an output file is specified - \ No newline at end of file diff --git a/src/backend/lambda_function.py b/src/backend/lambda_function.py index 6f129eb..1109f44 100644 --- a/src/backend/lambda_function.py +++ b/src/backend/lambda_function.py @@ -48,6 +48,12 @@ def handler(event, context): print("Attempting to download") s3.download_file(source_bucket, object_key, file_name) + user_uuid = "" + if "user_uuid" in event: + user_uuid = event["user_uuid"] + "/" + + print("user_uuid: " + user_uuid) + print("setup conversion function") target_bucket = 'opensim-viewer-public-download' print("file_name", file_name) @@ -62,7 +68,8 @@ def handler(event, context): gltfJson.save(destinationFile) print("Gltf file saved") destinationFileName = Path(file_name).with_suffix('.gltf') - strDestinationFileName = str(destinationFileName).split('/')[-1] + strDestinationFileName = user_uuid + str(destinationFileName).split('/')[-1] + print("Destination File Name: " + strDestinationFileName) # print("DestinationFile string", strDestinationFileName) s3.upload_file(destinationFile, target_bucket, strDestinationFileName) # print("File upload launched") diff --git a/src/backend/readme.md b/src/backend/readme.md deleted file mode 100644 index b2e4949..0000000 --- a/src/backend/readme.md +++ /dev/null @@ -1,59 +0,0 @@ -*Requisites:** Conda and python installed. - -1. Create environment using the `environment.yml` file: - - `conda env create -f environment.yml` - -2. Activate environment: - - `conda activate opensim-viewer-bend` - -3. Start server: - - `python manage.py runserver` - -### Instructions for database migration - - 1. Create migration files: - - `python manage.py makemigrations` - - 2. Migrate the database (warning: data can be lost) - - `python manage.py migrate` - -### Instructions for recreating ERD diagram - -Instructions in this [Link](https://www.wplogout.com/export-database-diagrams-erd-from-django/). - -### Instructions for localization - -Instructions in this [Link](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/). - -Inside of backend app folder: - -1. Create files for a language: - - `django-admin makemessages -l ` - -2. Compile messages: - - `django-admin compilemessages` - -### Instruction for testing - -- Execute all tests: - - `python manage.py test --verbosity=0` - - -General -------- -- This folder contains scripts to convert OpenSim based data files into gltf format -- We use third party library pygltflib to manipulate the gltf structure thus avoiding low level json file manipulation and encoding/decoding whenever possible. - -Description of specific python files: -------------------------------------- -- openSimData2Gltf.py: gneric utilities that take columns of data and time stamps and create the corresponding Accessors in the passed in GLTF2 structure -- convert{xxx}2Gltf.py: Utilities for converting files with extension {xxx} to gltf, the convention is to produce a file with the same name but with different extension, unless an output file is specified - \ No newline at end of file diff --git a/src/components/Components/DropFile.tsx b/src/components/Components/DropFile.tsx index 69aa155..9c4300d 100644 --- a/src/components/Components/DropFile.tsx +++ b/src/components/Components/DropFile.tsx @@ -20,7 +20,11 @@ const lambda = new AWS.Lambda({ region: 'us-west-2', // replace with your region }); -const FileDropArea = observer(() => { +interface FileDropAreaProps { + paddingY?: number; +} + +const FileDropArea: React.FC =observer(({ paddingY = 16}) => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); @@ -108,16 +112,15 @@ const FileDropArea = observer(() => { viewerState.isLocalUpload = true } else { Storage.put(file.name, file).then(()=>{ - /* - const api_url = 'https://eudfxg3a9l.execute-api.us-west-2.amazonaws.com/dev/' - axios.post(api_url, data).then(response => { - const gltf_url = response.data['url']; .replace(/\.\w+$/, '.gltf') - appState.setCurrentModelPath(gltf_url); */ + + let user_uuid = viewerState.user_uuid; + const params: AWS.Lambda.InvocationRequest = { FunctionName: 'opensim-viewer-func', // replace with your Lambda function's name Payload: JSON.stringify({ s3: 'opensimviewer-input-bucket101047-dev', - key: 'public/'+file.name + key: 'public/' + file.name, + user_uuid: user_uuid }) }; lambda.invoke(params, (err: any, data: any) => { @@ -125,7 +128,7 @@ const FileDropArea = observer(() => { console.error(err); } else { const key = file.name.replace(/\.\w+$/, '.gltf') - const gltf_url = "https://s3.us-west-2.amazonaws.com/opensim-viewer-public-download/"+key + const gltf_url = "https://s3.us-west-2.amazonaws.com/opensim-viewer-public-download/" + user_uuid + "/"+key /* appState.setCurrentModelPath(gltf_url); */ navigate("/viewer/"+encodeURIComponent(gltf_url)) console.log('Lambda function invoked successfully:', data); @@ -152,7 +155,7 @@ const FileDropArea = observer(() => { sx={{ border: '1px dashed gray', borderRadius: '4px', - padding: '16px', + padding: `${paddingY}px`, textAlign: 'center', cursor: 'pointer', }} diff --git a/src/components/Components/FloatingControlsPanel.css b/src/components/Components/FloatingControlsPanel.css index 0588b63..c59e2ae 100644 --- a/src/components/Components/FloatingControlsPanel.css +++ b/src/components/Components/FloatingControlsPanel.css @@ -1,7 +1,6 @@ /* FloatingButton.css */ .floating-buttons-container { position: absolute; - top: 80px; right: 25px; z-index: 999; } diff --git a/src/components/Components/FloatingControlsPanel.tsx b/src/components/Components/FloatingControlsPanel.tsx index 70ebce6..6df288f 100644 --- a/src/components/Components/FloatingControlsPanel.tsx +++ b/src/components/Components/FloatingControlsPanel.tsx @@ -16,6 +16,7 @@ import { ModelInfo } from '../../state/ModelUIState'; interface FloatingControlsPanelProps { videoRecorderRef: any; info: ModelInfo; + top: string; } function FloatingControlsPanel(props :FloatingControlsPanelProps) { @@ -29,7 +30,7 @@ function FloatingControlsPanel(props :FloatingControlsPanelProps) { }; return ( -
+
diff --git a/src/components/pages/BottomBar.tsx b/src/components/pages/BottomBar.tsx index 78ffc73..269d32c 100644 --- a/src/components/pages/BottomBar.tsx +++ b/src/components/pages/BottomBar.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react' import { AnimationClip } from 'three'; import { useTranslation } from 'react-i18next'; import { useModelContext } from '../../state/ModelUIStateContext'; +import { Camera } from 'three/src/cameras/Camera' import React, { useCallback, useRef } from 'react'; const NonAnimatedSlider = styled(Slider)(({ theme } : {theme:any}) => ({ @@ -39,6 +40,7 @@ const BottomBar = React.forwardRef(function CustomContent( const [speed, setSpeed] = useState(1.0); const [play, setPlay] = useState(false); const [selectedAnim, setSelectedAnim] = useState(""); + const [selectedCam, setSelectedCam] = useState(""); const isExtraSmallScreen = useMediaQuery((theme:any) => theme.breakpoints.only('xs')); const isSmallScreen = useMediaQuery((theme:any) => theme.breakpoints.only('sm')); @@ -47,6 +49,7 @@ const BottomBar = React.forwardRef(function CustomContent( const minWidthSlider = isExtraSmallScreen ? 150 : isSmallScreen ? 175 : isMediumScreen ? 250 : 300; // Adjust values as needed const maxWidthTime = 45; + const handleAnimationChange = useCallback((animationName: string, animate: boolean) => { const targetName = animationName setSelectedAnim(animationName); @@ -63,11 +66,29 @@ const BottomBar = React.forwardRef(function CustomContent( //setAge(event.target.value as string); }, [curState]); + const handleCameraChange = useCallback((cameraName: string) => { + const targetName = cameraName + setSelectedCam(cameraName); + + const idx = curState.cameras.findIndex((value: Camera, index: number)=>{return (value.name === targetName)}) + if (idx !== -1) { + curState.setCurrentCameraIndex(idx) + } + + curState.setCurrentFrame(0); + //setAge(event.target.value as string); + }, [curState]); + const handleAnimationChangeEvent = (event: SelectChangeEvent) => { const targetName = event.target.value as string handleAnimationChange(targetName, true) }; + const handleCameraChangeEvent = (event: SelectChangeEvent) => { + const targetName = event.target.value as string + handleCameraChange(targetName) + }; + function togglePlayAnimation() { curState.setAnimating(!curState.animating); setPlay(!play); @@ -101,11 +122,19 @@ const BottomBar = React.forwardRef(function CustomContent( } }, [curState.animations, handleAnimationChange]); + useEffect(() => { + if (curState.cameras.length > 0) { + setSelectedCam(curState.cameras[0].name) + handleCameraChange(curState.cameras[0].name) + } + }, [curState.cameras, handleCameraChange]); + return ( + { curState.animations.length < 1 ? null : ( + {curState.cameras.map(cam => ( + + {cam.name} + + ))} + visibility={false} + + + + )} diff --git a/src/components/pages/HomePage.tsx b/src/components/pages/HomePage.tsx index 6b92d1e..62168c4 100644 --- a/src/components/pages/HomePage.tsx +++ b/src/components/pages/HomePage.tsx @@ -8,7 +8,7 @@ const HomePage = () => { {t('welcome_title')} - + ) diff --git a/src/components/pages/ModelViewPage.tsx b/src/components/pages/ModelViewPage.tsx index e04e7cc..3b885af 100644 --- a/src/components/pages/ModelViewPage.tsx +++ b/src/components/pages/ModelViewPage.tsx @@ -53,39 +53,62 @@ interface ViewerProps { export function ModelViewPage({url, embedded, noFloor}:ViewerProps) { const bottomBarRef = useRef(null); + const videoRecorderRef = useRef(null); + + + // TODO: Move to a general styles file? + const leftMenuWidth = 60; + const drawerContentWidth = 250; + + const [heightBottomBar, setHeightBottomBar] = useState(0); const theme = useTheme(); const curState = useModelContext(); let { urlParam } = useParams(); - const [heightBottomBar, setHeightBottomBar] = useState(0); + const [uiState] = React.useState(curState); + const [menuOpen, setMenuOpen] = React.useState(false); + const [selectedTabName, setSelectedTabName] = React.useState("File"); + + const [ displaySideBar, setDisplaySideBar ] = useState('inherit'); + const [canvasWidth, setCanvasWidth] = useState("calc(100vw - " + (leftMenuWidth + (menuOpen ? drawerContentWidth : 0)) + "px)"); + const [canvasHeight, setCanvasHeight] = useState("calc(100vh - 68px - " + heightBottomBar + "px)"); + const [canvasLeft, setCanvasLeft] = useState(leftMenuWidth + (menuOpen ? drawerContentWidth : 0)); + const [floatingButtonsContainerTop, setFloatingButtonsContainerTop] = useState("80px"); useEffect(() => { if (bottomBarRef.current) { const heightBottomBar = bottomBarRef.current.offsetHeight; setHeightBottomBar(bottomBarRef.current.offsetHeight); + setCanvasHeight("calc(100vh - 68px - " + heightBottomBar + "px)"); + // Do something with heightBottomBar if needed console.log('Height of BottomBar:', heightBottomBar); } }, []); + React.useEffect(() => { + // Change interface if we are in GUI mode. + if (viewerState.isGuiMode) { + setDisplaySideBar('none'); + setCanvasWidth('100%'); + setCanvasHeight('calc(100vh - 68px)'); + setCanvasLeft(0); + setFloatingButtonsContainerTop("12px") + } + }, []); + //console.log(urlParam); if (urlParam!== undefined) { var decodedUrl = decodeURIComponent(urlParam); viewerState.setCurrentModelPath(decodedUrl); curState.setCurrentModelPath(viewerState.currentModelPath); // If urlParam is not undefined, this means it is getting the model from S3 and not from local. - viewerState.isLocalUpload = false; + viewerState.setIsLocalUpload(false); } else curState.setCurrentModelPath(viewerState.currentModelPath); - const [uiState] = React.useState(curState); - const [menuOpen, setMenuOpen] = React.useState(false); - const [selectedTabName, setSelectedTabName] = React.useState("File"); - - const videoRecorderRef = useRef(null); - function toggleOpenMenu(name: string = "") { // If same name, or empty just toggle. if (name === selectedTabName || name === "") setMenuOpen(!menuOpen); @@ -94,39 +117,35 @@ export function ModelViewPage({url, embedded, noFloor}:ViewerProps) { // Always store same name. setSelectedTabName(name); } - - // TODO: Move to a general styles file? - const leftMenuWidth = 60; - const drawerContentWidth = 250; - return (
- +
+ +
+ info={new ModelInfo(uiState.modelInfo.model_name, uiState.modelInfo.desc, uiState.modelInfo.authors)} + top={floatingButtonsContainerTop}/> = ({ currentModelPath, supportCo // useGLTF suspends the component, it literally stops processing const { scene, animations } = useGLTF(currentModelPath); + const { set, gl, camera } = useThree(); const no_face_cull = (scene: Group)=>{ if (scene) { scene.traverse((o)=>{ @@ -23,10 +28,11 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo o.frustumCulled = false; } mapObjectToLayer(o) - + }) } }; + const LayerMap = new Map([ ["Mesh", 1], ["Force", 2], @@ -56,6 +62,17 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo } } no_face_cull(scene); + + const applyAnimationColors = ()=>{ + colorNodeMap.forEach((node)=>{ + if (node instanceof Mesh){ + //console.log(node.material.color); + //console.log(node); + const newColor = new Color(node.position.x, node.position.y, node.position.z); + node.material.color = newColor + } + }) + } // eslint-disable-next-line no-mixed-operators const [sceneObjectMap] = useState>(new Map()); const [objectSelectionBox, setObjectSelectionBox] = useState(new BoxHelper(scene)); @@ -63,14 +80,97 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo const [animationIndex, setAnimationIndex] = useState(-1) const [startTime, setStartTime] = useState(0) const [mixers, ] = useState([]) + const [colorNodeMap] = useState>(new Map()); let curState = useModelContext(); curState.scene = scene; + const [currentCamera, setCurrentCamera] = useState() + + + // This useEffect loads the cameras and assign them to its respective states. + useEffect(() => { + const cameras = scene.getObjectsByProperty( 'isPerspectiveCamera', true ) + if (cameras.length > 0) { + // Get the canvas element from the gl + var canvas = gl.domElement; + // Calculate the aspect ratio + var aspectRatio = canvas.clientWidth / canvas.clientHeight; + // Set aspectRatio to cameras + cameras.forEach(function(camera) { + const cameraPers = camera as PerspectiveCamera + cameraPers.aspect = aspectRatio; + cameraPers.updateProjectionMatrix(); + }); + // Update cameras list. + curState.setCamerasList(cameras.map(obj => obj as PerspectiveCamera)) + // Set current camera and current index as 0 + setCurrentCamera(cameras.length > 0 ? cameras[0] as PerspectiveCamera : new PerspectiveCamera()) + curState.setCurrentCameraIndex(0) + } + }, [curState, scene, gl.domElement.clientWidth, gl.domElement, set]); + + // This useEffect sets the current selected camera. + useEffect(() => { + if (curState.cameras.length > 0 && currentCamera) { + const selectedCamera = curState.cameras[curState.currentCameraIndex] as PerspectiveCamera; + setCurrentCamera(selectedCamera); + set({ camera: selectedCamera }); + + animations.forEach((clip) => { + clip.tracks.forEach((track) => { + if (track.name.includes(selectedCamera.name)) { + if (track.name.endsWith('.position')) { + // Extract initial position + const initialPosition = new THREE.Vector3( + track.values[0], + track.values[1], + track.values[2] + ); + console.log("INITIAL") + console.log(initialPosition) + selectedCamera.position.copy(initialPosition); + } + + if (track.name.endsWith('.quaternion')) { + // Extract initial rotation (quaternion) + const initialRotation = new THREE.Quaternion( + track.values[0], + track.values[1], + track.values[2], + track.values[3] + ); + console.log("INITIAL") + console.log(initialRotation) + selectedCamera.quaternion.copy(initialRotation); + } + + if (track.name.endsWith('.rotation')) { + // Extract initial rotation (Euler) + const initialRotation = new THREE.Euler( + track.values[0], + track.values[1], + track.values[2] + ); + console.log("INITIAL") + console.log(initialRotation) + selectedCamera.rotation.copy(initialRotation); + } + } + }); + }); + } + }, [currentCamera, set, curState.currentCameraIndex, curState.cameras, animations]); + + if (supportControls) { scene.traverse((o) => { - sceneObjectMap.set(o.uuid, o) + sceneObjectMap.set(o.uuid, o); + if (o.name.startsWith("ColorNode")) { + colorNodeMap.set(o.uuid, o); + } } ) + if (objectSelectionBox !== null) { objectSelectionBox.visible = false; scene.add(objectSelectionBox!); @@ -88,6 +188,7 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo curState.setModelInfo(modelData.name, desc, authors) } } + // Make sure mixers match animations if ((animations.length > 0 && mixers.length !==animations.length) || (animations.length > 0 && mixers.length > 0 && mixers[0].getRoot() !== scene)) { @@ -101,6 +202,8 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo } useFrame((state, delta) => { + console.log(camera.position) + console.log(camera.rotation) if (!useEffectRunning) { if (curState !== undefined) { if (supportControls ) { @@ -141,6 +244,8 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo const currentTime = mixers[curState.currentAnimationIndex].clipAction(animations[curState.currentAnimationIndex]).time mixers[curState.currentAnimationIndex].update(delta * curState.animationSpeed) console.log(duration) + // For material at index "key" setColor to nodes["value"].translation + applyAnimationColors(); curState.setCurrentFrame(Math.trunc((currentTime / duration) * 100)) setStartTime(Math.trunc((currentTime / duration) * 100)) } @@ -150,10 +255,11 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo let duration = mixers[curState.currentAnimationIndex]?.clipAction(animations[curState.currentAnimationIndex]).getClip().duration; const framePercentage = curState.currentFrame / 100; const currentTime = duration * framePercentage; + // For material at index "key" setColor to nodes["value"].translation + applyAnimationColors(); mixers[curState.currentAnimationIndex].clipAction(animations[curState.currentAnimationIndex]).time = currentTime; setStartTime(curState.currentFrame) mixers[curState.currentAnimationIndex].update(delta * curState.animationSpeed) - } } } @@ -196,4 +302,4 @@ const OpenSimScene: React.FC = ({ currentModelPath, supportCo } -export default OpenSimScene +export default observer(OpenSimScene) diff --git a/src/gui.css b/src/gui.css new file mode 100644 index 0000000..ba980f1 --- /dev/null +++ b/src/gui.css @@ -0,0 +1,44 @@ +#opensim-appbar-visibility { + display: none; +} + +#opensim-modelview-sidebar { + display: none; +} + +#canvas-element { + width: 100% !important; + left: 0 !important; + height: calc(100vh - 68px) !important; +} + +.floating-buttons-container { + top: 12px !important; +} + +.App { + text-align: center; + height: 100vh; + width: 100vw; +} + +.App-logo { + height: 10vmin; + pointer-events: none; +} + + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} diff --git a/src/internationalization/i18n.js b/src/internationalization/i18n.js index 3553de2..5f1471b 100644 --- a/src/internationalization/i18n.js +++ b/src/internationalization/i18n.js @@ -57,8 +57,8 @@ i18next zoomOut: "Zoom Out", measure: "Measure", annotate: "Annotate", - snapshoot: "Snapshoot", - record: "record", + snapshoot: "Snapshot", + record: "Record", }, login: { title: "Login", diff --git a/src/state/ModelUIState.tsx b/src/state/ModelUIState.tsx index f3e5358..6d0d9f4 100644 --- a/src/state/ModelUIState.tsx +++ b/src/state/ModelUIState.tsx @@ -1,6 +1,7 @@ import { makeObservable, observable, action } from 'mobx' import SceneTreeModel from '../helpers/SceneTreeModel' import { AnimationClip } from 'three/src/animation/AnimationClip' +import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera' import { Group } from 'three' export class ModelInfo { @@ -26,6 +27,8 @@ export class ModelUIState { animationSpeed: number animations: AnimationClip[] currentAnimationIndex: number + cameras: PerspectiveCamera[] + currentCameraIndex: number selected: string deSelected: string cameraLayersMask: number @@ -47,6 +50,8 @@ export class ModelUIState { this.animationSpeed = 1.0 this.animations = [] this.currentAnimationIndex = -1 + this.cameras = [] + this.currentCameraIndex = -1 this.selected = "" this.deSelected = "" this.cameraLayersMask = -1 @@ -64,13 +69,17 @@ export class ModelUIState { setAnimationList: observable, setAnimationSpeed: action, animations: observable, + cameras: observable, + setCamerasList: action, selected: observable, setSelected: action, sceneTree: observable, setSceneTree: action, cameraLayersMask: observable, currentFrame: observable, - setCurrentFrame: action + setCurrentFrame: action, + currentCameraIndex: observable, + setCurrentCameraIndex: action }) console.log("Created ModelUIState instance ", currentModelPathState) } @@ -83,8 +92,10 @@ export class ModelUIState { this.cameraLayersMask = -1 this.animating = false this.animationSpeed = 1 - this.animations = [] + this.animations = [] this.currentAnimationIndex = -1 + this.cameras = [] + this.currentCameraIndex = -1 } } setRotating(newState: boolean) { @@ -105,6 +116,9 @@ export class ModelUIState { setCurrentAnimationIndex(newIndex: number) { this.currentAnimationIndex = newIndex } + setCurrentCameraIndex(newIndex: number) { + this.currentCameraIndex = newIndex + } setShowGlobalFrame(newState: boolean) { this.showGlobalFrame = newState } @@ -114,6 +128,9 @@ export class ModelUIState { setAnimationList(animations: AnimationClip[]) { this.animations=animations } + setCamerasList(cameras: PerspectiveCamera[]) { + this.cameras=cameras + } setAnimationSpeed(newSpeed: number) { this.animationSpeed = newSpeed } diff --git a/src/state/ViewerState.tsx b/src/state/ViewerState.tsx index 41c2533..2e2fd73 100644 --- a/src/state/ViewerState.tsx +++ b/src/state/ViewerState.tsx @@ -13,6 +13,8 @@ class ViewerState { recordedVideoFormat: string isRecordingVideo: boolean isProcessingVideo: boolean + isGuiMode: boolean + user_uuid: string constructor( currentModelPathState: string, @@ -26,6 +28,7 @@ class ViewerState { recordedVideoName: string, recordedVideoFormat: string, isRecordingVideo: boolean, + isGuiMode: boolean, isProcessingVideo: boolean ) { this.currentModelPath = currentModelPathState @@ -39,7 +42,9 @@ class ViewerState { this.recordedVideoName = recordedVideoName this.recordedVideoFormat = recordedVideoFormat this.isRecordingVideo = isRecordingVideo + this.isGuiMode = isGuiMode this.isProcessingVideo = isProcessingVideo + this.user_uuid = '' makeObservable(this, { currentModelPath: observable, featuredModelsFilePath: observable, @@ -53,11 +58,13 @@ class ViewerState { setSnapshotFormat: action, setRecordedVideoName: action, setRecordedVideoFormat: action, + setIsLoggedIn: action, snapshotName: observable, snapshotFormat: observable, recordedVideoName: observable, recordedVideoFormat: observable, isRecordingVideo: observable, + isGuiMode: observable, isProcessingVideo: observable, setIsProcessingVideo: action, setIsRecordingVideo: action, @@ -78,6 +85,19 @@ class ViewerState { } setIsLoggedIn(newState: boolean) { this.isLoggedIn = newState + if (this.isLoggedIn){ + // Cache user_uuid until logout + const userName = localStorage.getItem('CognitoIdentityServiceProvider.6jlm2jeibh9aqb0dg34q2uf8pu.LastAuthUser'); + const storedDataString = localStorage.getItem('CognitoIdentityServiceProvider.6jlm2jeibh9aqb0dg34q2uf8pu.'+userName+'.userData'); + if (storedDataString != null) { + let storedData = JSON.parse(storedDataString); + storedData["UserAttributes"].forEach((element:any) => { + if (element["Name"] === "sub") { + this.user_uuid = element["Value"]; + } + }); + } + } } setIsFullScreen(newState: boolean) { this.isFullScreen = newState @@ -97,11 +117,14 @@ class ViewerState { setIsProcessingVideo(newState: boolean) { this.isProcessingVideo = newState } + setIsGuiMode(newState: boolean) { + this.isGuiMode = newState + } setIsRecordingVideo(newState: boolean) { this.isRecordingVideo = newState } } -const viewerState = new ViewerState('/builtin/arm26_elbow_flex.gltf', '/builtin/featured-models.json', false, false, false, false, "opensim-viewer-snapshot", 'png', "opensim-viewer-video", 'mp4', false, false) +const viewerState = new ViewerState('/builtin/arm26_elbow_flex.gltf', '/builtin/featured-models.json', false, false, false, false, "opensim-viewer-snapshot", 'png', "opensim-viewer-video", 'mp4', false, false, false) export default viewerState