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: {