diff --git a/RELEASE.md b/RELEASE.md index 111135f..5a1726d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -82,7 +82,6 @@ Here is a summary of the steps to cut a new release: - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. - - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: ```text diff --git a/package.json b/package.json index 4811d04..961a5ca 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "three": "^0.162.0", "ts-xor": "^1.3.0", "unique-names-generator": "^4.7.1", - "use-file-picker": "^2.1.2" + "use-file-picker": "^2.1.2", + "zustand": "^4.5.2" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", diff --git a/src/arCube.ts b/src/arCube.ts index 6e8ca7e..b8d2bd9 100644 --- a/src/arCube.ts +++ b/src/arCube.ts @@ -1,7 +1,4 @@ -import { - selectAppData, - selectIsSomeoneScreenSharing -} from '@100mslive/react-sdk'; +import { selectIsSomeoneScreenSharing } from '@100mslive/react-sdk'; //@ts-expect-error AR.js doesn't have type definitions import * as THREEx from '@ar-js-org/ar.js/three.js/build/ar-threex.js'; import { IThemeManager } from '@jupyterlab/apputils'; @@ -10,9 +7,9 @@ import { ISignal, Signal } from '@lumino/signaling'; import * as THREE from 'three'; import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry'; import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; -import { APP_DATA } from './constants'; -import { hmsActions, hmsStore } from './hms'; +import { hmsStore } from './hms'; import { IModelRegistryData } from './registry'; +import { useCubeStore } from './store'; const FIRST_SCENE = 0; const SECOND_SCENE = 1; @@ -29,19 +26,9 @@ class ArCube { * Construct a new JupyterLab-Gather widget. */ constructor() { - this.secondSceneSignal = new Signal(this); this.scaleSignal = new Signal(this); this.bgCubeCenter = new THREE.Vector3(); this.initialize(); - - // this.animate(); - // window.addEventListener('markerFound', () => { - // console.log('Marker found'); - // }); - - // window.addEventListener('markerLost', () => { - // console.log('Marker lost'); - // }); } modelInScene: string[]; @@ -66,59 +53,45 @@ class ArCube { resolve: any; deltaTime: number; totalTime: number; - okToLoadModel: boolean; renderTarget: THREE.WebGLRenderTarget; sceneGroups: THREE.Group[]; isSecondScene: boolean; bgCubeBoundingBox: THREE.Box3; readonly existingWebcam: HTMLVideoElement | null; readonly newWebcam: HTMLVideoElement | undefined; - readonly secondSceneSignal: Signal; readonly scaleSignal: Signal; bgCubeCenter: THREE.Vector3; arjsVid: HTMLElement | null; - // sceneGroup: THREE.Group; - // sceneGroupArray: THREE.Group[]; - // edgeGroup: THREE.Group; - // gltfModel: THREE.Group; - // observer: IntersectionObserver; - // readonly markerControls: any; - // readonly ambientLight: THREE.AmbientLight; - // readonly rotationArray: THREE.Vector3[]; - // readonly markerRoot: THREE.Group; - // readonly markerGroup: THREE.Group; - // readonly pointLight: THREE.PointLight; - // readonly loader: THREE.TextureLoader; - // readonly stageMesh: THREE.MeshBasicMaterial; - // readonly stage: THREE.Mesh; - // readonly edgeGeometry: THREE.CylinderGeometry; - // readonly edgeCenters: THREE.Vector3[]; - // readonly edgeRotations: THREE.Vector3[]; - // readonly animationRequestId: number | undefined; - // readonly now: number; - // readonly then: number; - // readonly elapsed: number; - // readonly fpsInterval: number; - // readonly webcamFromArjs: HTMLElement | null; - // model: IModelRegistryData; + videoDeviceIdUnsub: () => void; + isSecondSceneUnsub: () => void; themeChangedSignal: ISignal< IThemeManager, IChangedArgs - >; + > | null; initialize() { this.sceneGroups = []; this.modelInScene = new Array(2); this.scenesWithModel = {}; - hmsStore.subscribe( - this.setupSource.bind(this), - selectAppData(APP_DATA.videoDeviceId) + + this.videoDeviceIdUnsub = useCubeStore.subscribe( + state => state.videoDeviceId, + videoDeviceId => { + console.log('dev - videoDeviceId', videoDeviceId); + this.setupSource(); + } ); - this.themeChangedSignal = hmsStore.getState( - selectAppData(APP_DATA.themeChanged) + this.isSecondSceneUnsub = useCubeStore.subscribe( + state => state.isSecondScene, + isSecondScene => (this.isSecondScene = isSecondScene) ); - this.themeChangedSignal.connect(this.handleThemeChange.bind(this)); + + this.themeChangedSignal = useCubeStore.getState().themeChangedSignal; + + this.themeChangedSignal + ? this.themeChangedSignal.connect(this.handleThemeChange.bind(this)) + : console.log('Theme change signal not found'); this.setupThreeStuff(); @@ -131,10 +104,21 @@ class ArCube { this.setupScene(FIRST_SCENE); } + cleanUp() { + this.videoDeviceIdUnsub(); + this.isSecondSceneUnsub(); + + useCubeStore.setState({ + canLoadModel: true, + modelInScene: [], + scenesWithModel: {}, + isSecondScene: false + }); + } + setupThreeStuff() { console.log('setting up three stuff'); - this.okToLoadModel = true; this.scene = new THREE.Scene(); // promise to track if AR.js has loaded the webcam @@ -199,13 +183,11 @@ class ArCube { this.clock = new THREE.Clock(); this.deltaTime = 0; this.totalTime = 0; - - hmsActions.setAppData(APP_DATA.renderer, this.renderer); } setupSource() { console.log('setting up source'); - const deviceId = hmsStore.getState(selectAppData(APP_DATA.videoDeviceId)); + const deviceId = useCubeStore.getState().videoDeviceId; this.arToolkitSource = new THREEx.ArToolkitSource({ sourceType: 'webcam', @@ -228,22 +210,6 @@ class ArCube { return cubeColorValue; } - // updateSource() { - // const deviceId = hmsStore.getState(selectAppData('videoDeviceId')); - - // this.arToolkitSource = new THREEx.ArToolkitSource({ - // sourceType: 'webcam', - // deviceId - // }); - - // this.arjsVid = document.getElementById('arjs-video'); - - // if (this.arjsVid) { - // this.arjsVid.remove(); - // } - // this.arToolkitSource.init(); - // } - setupContext() { console.log('setting up context'); @@ -390,10 +356,9 @@ class ArCube { // } // load model - this.okToLoadModel = false; - hmsActions.setAppData(APP_DATA.canLoadModel, false); + useCubeStore.setState({ canLoadModel: false }); - if ('url' in model) { + if ('url' in model!) { this.gltfLoader.load( model.url, gltf => { @@ -406,7 +371,7 @@ class ArCube { console.log('Error loading model url', error); } ); - } else if ('gltf' in model) { + } else if ('gltf' in model!) { // const data = JSON.stringify(model.gltf); const data = model.gltf; this.gltfLoader.parse( @@ -462,7 +427,6 @@ class ArCube { // add model to scene this.sceneGroups[sceneNumber].add(gltfModel); - this.okToLoadModel = true; // Track which scenes a model is loaded in // This is mostly to reflect changes to a model in JupyterCAD if it's loaded in multiple scenes @@ -484,10 +448,11 @@ class ArCube { // Track which model is loaded in which scene // This is to get model names on the scale sliders this.modelInScene[sceneNumber] = modelName; + useCubeStore.setState({ modelInScene: this.modelInScene }); // update app data state - hmsActions.setAppData(APP_DATA.loadedModels, updatedScenesWithModel); - hmsActions.setAppData(APP_DATA.canLoadModel, true); + useCubeStore.setState({ canLoadModel: true }); + useCubeStore.setState({ scenesWithModel: updatedScenesWithModel }); // Send scale value to right sidebar this.scaleSignal.emit({ sceneNumber, scale: minRatio }); @@ -522,15 +487,14 @@ class ArCube { } findModelByName(name: string) { - const modelRegistry = hmsStore.getState( - selectAppData(APP_DATA.modelRegistry) - ); + const modelRegistry = useCubeStore.getState().modelRegistry; return modelRegistry.find( (model: IModelRegistryData) => model.name === name ); } changeModelInScene(sceneNumber: number, modelName: string) { + console.log('dev - change model in scene', sceneNumber, modelName); // update tracking stuff const modelNameToRemove = this.modelInScene[sceneNumber]; const updatedModels = { ...this.scenesWithModel }; @@ -558,14 +522,12 @@ class ArCube { enableSecondScene() { console.log('enabling second'); - this.isSecondScene = true; this.setupScene(SECOND_SCENE); - this.secondSceneSignal.emit(true); + useCubeStore.setState({ isSecondScene: true }); } disableSecondScene() { console.log('disabling second'); - this.isSecondScene = false; //TODO this won't work with more than two scenes but it's fine for now this.sceneGroups.pop(); @@ -575,7 +537,7 @@ class ArCube { } }); - this.secondSceneSignal.emit(false); + useCubeStore.setState({ isSecondScene: false }); } resizeCanvasToDisplaySize() { diff --git a/src/arCubePlugin.ts b/src/arCubePlugin.ts index d8ab808..9c2489f 100644 --- a/src/arCubePlugin.ts +++ b/src/arCubePlugin.ts @@ -6,8 +6,7 @@ import { } from '@100mslive/hms-video-store'; import * as THREE from 'three'; import ArCube from './arCube'; -import { APP_DATA } from './constants'; -import { hmsActions } from './hms'; +import { useCubeStore } from './store'; class ArCubePlugin implements HMSVideoPlugin { input: HTMLCanvasElement | null; @@ -147,7 +146,7 @@ class ArCubePlugin implements HMSVideoPlugin { async init() { this.arCube = new ArCube(); - hmsActions.setAppData(APP_DATA.arCube, this.arCube); + useCubeStore.setState({ arCube: this.arCube }); this.arCube.animate(); } @@ -161,6 +160,8 @@ class ArCubePlugin implements HMSVideoPlugin { } stop() { + useCubeStore.setState({ arCube: null }); + // Remove video element added by AR.js const video = document.getElementById('arjs-video') as HTMLVideoElement; diff --git a/src/components/MainDisplay.tsx b/src/components/MainDisplay.tsx index 4d14899..fdf452f 100644 --- a/src/components/MainDisplay.tsx +++ b/src/components/MainDisplay.tsx @@ -1,5 +1,4 @@ import { - selectAppData, selectIsConnectedToRoom, selectSessionStore, useHMSActions, @@ -8,11 +7,12 @@ import { import { IStateDB } from '@jupyterlab/statedb'; import React, { useEffect } from 'react'; -import { APP_DATA, SESSION_STORE } from '../constants'; +import { SESSION_STORE } from '../constants'; import GridView from '../layouts/GridView'; import JoinFormView from '../layouts/JoinFormView'; import PresenterView from '../layouts/PresenterView'; import PreviewView from '../layouts/PreviewView'; +import { useCubeStore } from '../store'; import ControlBar from './ControlBar'; interface IMainDisplayProps { @@ -22,14 +22,16 @@ interface IMainDisplayProps { export const MainDisplay = ({ state }: IMainDisplayProps) => { const hmsActions = useHMSActions(); const isConnected = useHMSStore(selectIsConnectedToRoom); - const isConnecting = useHMSStore(selectAppData(APP_DATA.isConnecting)); const isPresenting = useHMSStore( selectSessionStore(SESSION_STORE.isPresenting) ); + const isConnecting = useCubeStore.use.isConnecting(); + const updateIsConnecting = useCubeStore.use.updateIsConnecting(); + useEffect(() => { if (isConnected) { - hmsActions.setAppData(APP_DATA.isConnecting, false); + updateIsConnecting(false); } }, [isConnected]); diff --git a/src/components/buttons/PluginButton.tsx b/src/components/buttons/PluginButton.tsx index 15db973..8c7dea4 100644 --- a/src/components/buttons/PluginButton.tsx +++ b/src/components/buttons/PluginButton.tsx @@ -13,6 +13,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { useEffect, useState } from 'react'; import ArCubePlugin from '../../arCubePlugin'; import { SESSION_STORE } from '../../constants'; +import { useCubeStore } from '../../store'; const PluginButton = () => { const hmsActions = useHMSActions(); @@ -33,6 +34,8 @@ const PluginButton = () => { const [isDisabled, setIsDisabled] = useState(false); const [prevRole, setPrevRole] = useState(); + const arCube = useCubeStore.use.arCube(); + useEffect(() => { hmsActions.sessionStore.observe(SESSION_STORE.isPresenting); hmsActions.sessionStore.observe(SESSION_STORE.presenterId); @@ -62,6 +65,7 @@ const PluginButton = () => { togglePresenterRole(prevRole?.name); hmsActions.sessionStore.set(SESSION_STORE.isPresenting, false); hmsActions.sessionStore.set(SESSION_STORE.presenterId, ''); + arCube?.cleanUp(); await hmsActions.removePluginFromVideoTrack(arPlugin); } diff --git a/src/components/modals/AddNewFileModal.tsx b/src/components/modals/AddNewFileModal.tsx index 693db4d..9a830d4 100644 --- a/src/components/modals/AddNewFileModal.tsx +++ b/src/components/modals/AddNewFileModal.tsx @@ -5,6 +5,7 @@ import { uniqueNamesGenerator } from 'unique-names-generator'; import { useFilePicker } from 'use-file-picker'; +//@ts-expect-error no types import { FileTypeValidator } from 'use-file-picker/validators'; import { IModelRegistryData } from '../../registry'; import Modal from './Modal'; diff --git a/src/components/modals/DeviceSettingModal.tsx b/src/components/modals/DeviceSettingModal.tsx index 5c50cef..b015c39 100644 --- a/src/components/modals/DeviceSettingModal.tsx +++ b/src/components/modals/DeviceSettingModal.tsx @@ -1,8 +1,7 @@ import { DeviceType, useDevices } from '@100mslive/react-sdk'; import React, { ChangeEvent, useEffect, useRef } from 'react'; -import { APP_DATA } from '../../constants'; -import { hmsActions } from '../../hms'; +import { useCubeStore } from '../../store'; import Modal from './Modal'; interface IAddNewModelModalProps { @@ -15,6 +14,7 @@ const DeviceSettingModal = ({ isOpen, onClose }: IAddNewModelModalProps) => { const { allDevices, updateDevice } = useDevices(); //TODO: Using for optimistic updates in the label. Other devices update quick enough const { videoInput, audioInput, audioOutput } = allDevices; + const updateDeviceId = useCubeStore.use.updateVideoDeviceId(); useEffect(() => { if (isOpen && focusInputRef.current) { @@ -27,7 +27,7 @@ const DeviceSettingModal = ({ isOpen, onClose }: IAddNewModelModalProps) => { const updateDeviceOnChange = (deviceId: string, deviceType: DeviceType) => { updateDevice({ deviceId, deviceType }); if (deviceType === DeviceType.videoInput) { - hmsActions.setAppData(APP_DATA.videoDeviceId, deviceId); + updateDeviceId(deviceId); } }; diff --git a/src/constants.ts b/src/constants.ts index d81307d..13f7b87 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,20 +18,7 @@ export const ROLES = { presenter: 'presenter' }; -export const APP_DATA = { - arCube: 'arCube', - canLoadModel: 'canLoadModel', - config: 'config', - isConnecting: 'isConnecting', - loadedModels: 'loadedModels', - modelRegistry: 'modelRegistry', - selectedModel: 'selectedModel', - renderer: 'renderer', - themeChanged: 'themeChanged', - videoDeviceId: 'videoDeviceId' -}; - export const SESSION_STORE = { isPresenting: 'isPresenting', presenterId: 'presenterId' -}; +} as const; diff --git a/src/index.ts b/src/index.ts index a222308..03b5f7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,12 +91,12 @@ const plugin: JupyterFrontEndPlugin = { // Register default models registry.registerModel({ name: 'duck', - url: 'https://github.khronos.org/glTF-Sample-Viewer-Release/assets/models/Models/Duck/glTF/Duck.gltf' + url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Duck/glTF-Binary/Duck.glb' }); registry.registerModel({ name: 'fox', - url: 'https://github.khronos.org/glTF-Sample-Viewer-Release/assets/models/Models/Fox/glTF/Fox.gltf' + url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Fox/glTF-Binary/Fox.glb' }); } }); diff --git a/src/layouts/JoinFormView.tsx b/src/layouts/JoinFormView.tsx index 92741eb..f40156e 100644 --- a/src/layouts/JoinFormView.tsx +++ b/src/layouts/JoinFormView.tsx @@ -8,7 +8,7 @@ import { colors, uniqueNamesGenerator } from 'unique-names-generator'; -import { APP_DATA } from '../constants'; +import { useCubeStore } from '../store'; interface IJoinFormViewProps { state: IStateDB; @@ -18,6 +18,9 @@ const JoinFormView = ({ state }: IJoinFormViewProps) => { const hmsActions = useHMSActions(); const [savedRoomCode, setSavedRoomCode] = useState(''); + const updateIsConnecting = useCubeStore.use.updateIsConnecting(); + const updateConfig = useCubeStore.use.updateConfig(); + const randomUserName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals], style: 'capital', @@ -59,7 +62,7 @@ const JoinFormView = ({ state }: IJoinFormViewProps) => { e.preventDefault(); console.log('clicking join'); - hmsActions.setAppData(APP_DATA.isConnecting, true); + updateIsConnecting(true); const { userName = '', roomCode = '' } = inputValues; @@ -71,14 +74,15 @@ const JoinFormView = ({ state }: IJoinFormViewProps) => { userName, authToken, captureNetworkQualityInPreview: true, - alwaysRequestPermissions: true, + rememberDeviceSelection: true, settings: { isAudioMuted: true, isVideoMuted: false }, metaData: '' }; - hmsActions.setAppData(APP_DATA.config, config); + + updateConfig(config); try { await hmsActions.preview({ ...config }); @@ -93,13 +97,13 @@ const JoinFormView = ({ state }: IJoinFormViewProps) => {
@@ -109,8 +113,9 @@ const JoinFormView = ({ state }: IJoinFormViewProps) => { className="jlab-gather-input" name="roomCode" placeholder="Room code" - onChange={handleInputChange} value={inputValues.roomCode} + onChange={handleInputChange} + required />