diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 2d0cce7572..5d21ef3a61 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -56,6 +56,8 @@ import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.ts import APS from "./aps/APS.ts" import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel.tsx" import Skybox from "./ui/components/Skybox.tsx" +import ProgressNotifications from "./ui/components/ProgressNotification.tsx" +import { ProgressHandle } from "./ui/components/ProgressNotificationData.ts" import ConfigureRobotModal from "./ui/modals/configuring/ConfigureRobotModal.tsx" import ResetAllInputsModal from "./ui/modals/configuring/ResetAllInputsModal.tsx" import ZoneConfigPanel from "./ui/panels/configuring/scoring/ZoneConfigPanel.tsx" @@ -107,12 +109,17 @@ function Synthesis() { } const setup = async () => { + const setupProgress = new ProgressHandle("Spawning Default Robot") + setupProgress.Update("Checking cache...", 0.1) + const info = await MirabufCachingService.CacheRemote(mira_path, MiraType.ROBOT) .catch(_ => MirabufCachingService.CacheRemote(DEFAULT_MIRA_PATH, MiraType.ROBOT)) .catch(console.error) const miraAssembly = await MirabufCachingService.Get(info!.id, MiraType.ROBOT) + setupProgress.Update("Parsing assembly...", 0.5) + await (async () => { if (!miraAssembly || !(miraAssembly instanceof mirabuf.Assembly)) { return @@ -121,11 +128,16 @@ function Synthesis() { const parser = new MirabufParser(miraAssembly) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`) + setupProgress.Fail("Failed to parse assembly") return } + setupProgress.Update("Creating scene object...", 0.9) + const mirabufSceneObject = new MirabufSceneObject(new MirabufInstance(parser), miraAssembly.info!.name!) World.SceneRenderer.RegisterSceneObject(mirabufSceneObject) + + setupProgress.Done() })() } @@ -186,6 +198,7 @@ function Synthesis() { {modalElement} )} + diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index fe4aaa6d2e..174cbcbff3 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -2,6 +2,7 @@ import * as THREE from "three" import { mirabuf } from "../proto/mirabuf" import MirabufParser, { ParseErrorSeverity } from "./MirabufParser.ts" import World from "@/systems/World.ts" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData.ts" type MirabufPartInstanceGUID = string @@ -107,7 +108,7 @@ class MirabufInstance { return this._batches } - public constructor(parser: MirabufParser, materialStyle?: MaterialStyle) { + public constructor(parser: MirabufParser, materialStyle?: MaterialStyle, progressHandle?: ProgressHandle) { if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) { throw new Error("Parser has significant errors...") } @@ -117,7 +118,10 @@ class MirabufInstance { this._meshes = new Map() this._batches = new Array() + progressHandle?.Update("Loading materials...", 0.4) this.LoadMaterials(materialStyle ?? MaterialStyle.Regular) + + progressHandle?.Update("Creating meshes...", 0.5) this.CreateMeshes() } @@ -236,8 +240,6 @@ class MirabufInstance { const batchedMesh = new THREE.BatchedMesh(count.maxInstances, count.maxVertices, count.maxIndices) this._batches.push(batchedMesh) - console.debug(`${count.maxInstances}, ${count.maxVertices}, ${count.maxIndices}`) - batchedMesh.material = material batchedMesh.castShadow = true batchedMesh.receiveShadow = true @@ -253,8 +255,6 @@ class MirabufInstance { batchedMesh.setMatrixAt(geoId, mat) - console.debug(geoId) - let bodies = this._meshes.get(instance.info!.GUID!) if (!bodies) { bodies = new Array<[THREE.BatchedMesh, number]>() diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index 8565a00335..4c4e18060f 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -1,6 +1,7 @@ import * as THREE from "three" import { mirabuf } from "@/proto/mirabuf" import { MirabufTransform_ThreeMatrix4 } from "@/util/TypeConversions" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" export type RigidNodeId = string @@ -72,11 +73,13 @@ class MirabufParser { return this._rootNode } - public constructor(assembly: mirabuf.Assembly) { + public constructor(assembly: mirabuf.Assembly, progressHandle?: ProgressHandle) { this._assembly = assembly this._errors = new Array() this._globalTransforms = new Map() + progressHandle?.Update("Parsing assembly...", 0.3) + this.GenerateTreeValues() this.LoadGlobalTransforms() diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 4010211884..f0bd7b5596 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -17,6 +17,7 @@ import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { MiraType } from "./MirabufLoader" import IntakeSensorSceneObject from "./IntakeSensorSceneObject" import EjectableSceneObject from "./EjectableSceneObject" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" const DEBUG_BODIES = false @@ -80,12 +81,14 @@ class MirabufSceneObject extends SceneObject { return this._mirabufInstance.parser.rootNode } - public constructor(mirabufInstance: MirabufInstance, assemblyName: string) { + public constructor(mirabufInstance: MirabufInstance, assemblyName: string, progressHandle?: ProgressHandle) { super() this._mirabufInstance = mirabufInstance this._assemblyName = assemblyName + progressHandle?.Update("Creating mechanism...", 0.9) + this._mechanism = World.PhysicsSystem.CreateMechanismFromParser(this._mirabufInstance.parser) if (this._mechanism.layerReserve) { this._physicsLayerReserve = this._mechanism.layerReserve @@ -366,14 +369,17 @@ class MirabufSceneObject extends SceneObject { } } -export async function CreateMirabuf(assembly: mirabuf.Assembly): Promise { - const parser = new MirabufParser(assembly) +export async function CreateMirabuf( + assembly: mirabuf.Assembly, + progressHandle?: ProgressHandle +): Promise { + const parser = new MirabufParser(assembly, progressHandle) if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`) return } - return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!) + return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle) } /** diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index f60447705c..ed2ea62bc5 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -91,7 +91,9 @@ describe("Mirabuf Physics Loading", () => { const assembly = await MirabufCachingService.CacheRemote( "/api/mira/Robots/Team 2471 (2018)_v7.mira", MiraType.ROBOT - ).then(x => MirabufCachingService.Get(x!.id, MiraType.ROBOT)) + ).then(x => { + return MirabufCachingService.Get(x!.id, MiraType.ROBOT) + }) const parser = new MirabufParser(assembly!) const physSystem = new PhysicsSystem() diff --git a/fission/src/ui/components/ProgressNotification.tsx b/fission/src/ui/components/ProgressNotification.tsx new file mode 100644 index 0000000000..bd55cf0eea --- /dev/null +++ b/fission/src/ui/components/ProgressNotification.tsx @@ -0,0 +1,155 @@ +import { styled, Typography } from "@mui/material" +import { Box } from "@mui/system" +import { useEffect, useReducer, useState } from "react" +import { ProgressHandle, ProgressHandleStatus, ProgressEvent } from "./ProgressNotificationData" +import { easeOutQuad } from "@/util/EasingFunctions" + +interface ProgressData { + lastValue: number + currentValue: number + lastUpdate: number +} + +const handleMap = new Map() + +const TypoStyled = styled(Typography)(_ => ({ + fontFamily: "Artifakt", + textAlign: "center", +})) + +interface NotificationProps { + handle: ProgressHandle +} + +function Interp(elapse: number, progressData: ProgressData) { + const [value, setValue] = useState(0) + + useEffect(() => { + const update = () => { + // Get the portion of the completed elapse timer, passed into an easing function. + const n = Math.min(1.0, Math.max(0.0, (Date.now() - progressData.lastUpdate) / elapse)) + // Convert the result of the easing function [0, 1] to a lerp from last value to current value + const v = progressData.lastValue + (progressData.currentValue - progressData.lastValue) * easeOutQuad(n) + + setValue(v) + } + + const interval = setInterval(update, 5) + const timeout = setTimeout(() => clearInterval(interval), elapse) + + return () => { + clearTimeout(timeout) + clearInterval(interval) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [progressData]) + + return value +} + +function ProgressNotification({ handle }: NotificationProps) { + const [progressData, setProgressData] = useState({ + lastValue: 0, + currentValue: 0, + lastUpdate: Date.now(), + }) + + const interpProgress = Interp(500, progressData) + + useEffect(() => { + setProgressData({ lastValue: progressData.currentValue, currentValue: handle.progress, lastUpdate: Date.now() }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handle.progress]) + + return ( + + + + {handle.title} + + {handle.message.length > 0 ? {handle.message} : <>} + + + + ) +} + +function ProgressNotifications() { + const [progressElements, updateProgressElements] = useReducer(() => { + return handleMap.size > 0 + ? [...handleMap.entries()].map(([_, handle]) => ( + + )) + : undefined + }, undefined) + + useEffect(() => { + const onHandleUpdate = (e: ProgressEvent) => { + const handle = e.handle + if (handle.status > 0) { + setTimeout(() => handleMap.delete(handle.handleId) && updateProgressElements(), 2000) + } + handleMap.set(handle.handleId, handle) + updateProgressElements() + } + + ProgressEvent.AddListener(onHandleUpdate) + return () => { + ProgressEvent.RemoveListener(onHandleUpdate) + } + }, [updateProgressElements]) + + return ( + + {progressElements ?? <>} + + ) +} + +export default ProgressNotifications diff --git a/fission/src/ui/components/ProgressNotificationData.ts b/fission/src/ui/components/ProgressNotificationData.ts new file mode 100644 index 0000000000..54963d58d7 --- /dev/null +++ b/fission/src/ui/components/ProgressNotificationData.ts @@ -0,0 +1,73 @@ +let nextHandleId = 0 + +export enum ProgressHandleStatus { + inProgress = 0, + Done = 1, + Error = 2, +} + +export class ProgressHandle { + private _handleId: number + private _title: string + public message: string = "" + public progress: number = 0.0 + public status: ProgressHandleStatus = ProgressHandleStatus.inProgress + + public get handleId() { + return this._handleId + } + public get title() { + return this._title + } + + public constructor(title: string) { + this._handleId = nextHandleId++ + this._title = title + + this.Push() + } + + public Update(message: string, progress: number, status?: ProgressHandleStatus) { + this.message = message + this.progress = progress + status && (this.status = status) + + this.Push() + } + + public Fail(message?: string) { + this.Update(message ?? "Failed", 1, ProgressHandleStatus.Error) + } + + public Done(message?: string) { + this.Update(message ?? "Done", 1, ProgressHandleStatus.Done) + } + + public Push() { + ProgressEvent.Dispatch(this) + } +} + +export class ProgressEvent extends Event { + public static readonly EVENT_KEY = "ProgressEvent" + + public handle: ProgressHandle + + private constructor(handle: ProgressHandle) { + super(ProgressEvent.EVENT_KEY) + + this.handle = handle + } + + public static Dispatch(handle: ProgressHandle) { + window.dispatchEvent(new ProgressEvent(handle)) + } + + public static AddListener(func: (e: ProgressEvent) => void) { + window.addEventListener(this.EVENT_KEY, func as (e: Event) => void) + } + + public static RemoveListener(func: (e: ProgressEvent) => void) { + window.removeEventListener(this.EVENT_KEY, func as (e: Event) => void) + } +} diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index e4498c7e97..faac3f954f 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -22,6 +22,7 @@ import Panel, { PanelPropsImpl } from "@/ui/components/Panel" import { usePanelControlContext } from "@/ui/PanelContext" import TaskStatus from "@/util/TaskStatus" import { BiRefresh } from "react-icons/bi" +import { ProgressHandle } from "@/ui/components/ProgressNotificationData" const DownloadIcon = const AddIcon = @@ -124,20 +125,30 @@ function GetCacheInfo(miraType: MiraType): MirabufCacheInfo[] { return Object.values(MirabufCachingService.GetCacheMap(miraType)) } -function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType) { - MirabufCachingService.Get(info.id, type).then(assembly => { - if (assembly) { - CreateMirabuf(assembly).then(x => { - if (x) { - World.SceneRenderer.RegisterSceneObject(x) - } - }) +function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { + if (!progressHandle) { + progressHandle = new ProgressHandle(info.name ?? info.cacheKey) + } + + MirabufCachingService.Get(info.id, type) + .then(assembly => { + if (assembly) { + CreateMirabuf(assembly).then(x => { + if (x) { + World.SceneRenderer.RegisterSceneObject(x) + progressHandle.Done() + } else { + progressHandle.Fail() + } + }) - if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) - } else { - console.error("Failed to spawn robot") - } - }) + if (!info.name) MirabufCachingService.CacheInfo(info.cacheKey, type, assembly.info?.name ?? undefined) + } else { + progressHandle.Fail() + console.error("Failed to spawn robot") + } + }) + .catch(() => progressHandle.Fail()) } const ImportMirabufPanel: React.FC = ({ panelId }) => { @@ -233,9 +244,18 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { // Cache a selected remote mirabuf assembly, load from cache. const selectRemote = useCallback( (info: MirabufRemoteInfo, type: MiraType) => { - MirabufCachingService.CacheRemote(info.src, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + const status = new ProgressHandle(info.displayName) + status.Update("Downloading from Synthesis...", 0.05) + + MirabufCachingService.CacheRemote(info.src, type) + .then(cacheInfo => { + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }) + .catch(() => status.Fail()) closePanel(panelId) }, @@ -244,9 +264,18 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const selectAPS = useCallback( (data: Data, type: MiraType) => { - MirabufCachingService.CacheAPS(data, type).then(cacheInfo => { - cacheInfo && SpawnCachedMira(cacheInfo, type) - }) + const status = new ProgressHandle(data.attributes.displayName ?? data.id) + status.Update("Downloading from APS...", 0.05) + + MirabufCachingService.CacheAPS(data, type) + .then(cacheInfo => { + if (cacheInfo) { + SpawnCachedMira(cacheInfo, type, status) + } else { + status.Fail("Failed to cache") + } + }) + .catch(() => status.Fail()) closePanel(panelId) }, diff --git a/fission/src/util/EasingFunctions.ts b/fission/src/util/EasingFunctions.ts new file mode 100644 index 0000000000..d1c3f95084 --- /dev/null +++ b/fission/src/util/EasingFunctions.ts @@ -0,0 +1,9 @@ +/** + * Source: https://easings.net/#easeOutQuad + * + * @param n Input of the easing function [0, 1] + * @returns -(n^2) + 2n + */ +export function easeOutQuad(n: number): number { + return 1 - (1 - n) * (1 - n) +} diff --git a/fission/vite.config.ts b/fission/vite.config.ts index 524c10306c..d1120aebe9 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ ] }, test: { + testTimeout: 5000, globals: true, environment: 'jsdom', browser: {