diff --git a/frontend/App.vue b/frontend/App.vue index e4debd3..b8c856b 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -33,6 +33,10 @@ const setDisableTap = (val: boolean) => disableTap.value = val; provide('disableTap', {disableTap, setDisableTap}); async function onModelUpdateRequest(event: NetworkUpdateEvent) { + // Trigger progress bar as soon as possible (also triggered earlier for each raw notification) + if (viewer.value && event.models.length > 0) { + viewer.value.onProgress(0.10); + } // Load/unload a new batch of models to optimize rendering time console.log("Received model update request", event.models); let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null); @@ -70,6 +74,8 @@ async function onModelRemoveRequest(name: string) { // Set up the load model event listener let networkMgr = new NetworkManager(); +networkMgr.addEventListener('update-early', + (e) => viewer.value?.onProgress((e as CustomEvent>).detail.length * 0.01)); networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent)); // Start loading all configured models ASAP for (let model of settings.preload) { diff --git a/frontend/misc/network.ts b/frontend/misc/network.ts index 6666e14..06df1b8 100644 --- a/frontend/misc/network.ts +++ b/frontend/misc/network.ts @@ -93,20 +93,22 @@ export class NetworkManager extends EventTarget { private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => { }) { - // console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash); - // Also update buffered updates to have only the latest one per model + console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove); + + // We only care about the latest update per model name this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name); - // Add the new model to the list of updates - let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove); - this.bufferedUpdates.push(newModel); + // Add the new model to the list of updates and dispatch the early update + let upd = new NetworkUpdateEventModel(name, url, hash, isRemove); + this.bufferedUpdates.push(upd); + this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates})); // Optimization: try to batch updates automatically for faster rendering if (this.batchTimeout !== null) clearTimeout(this.batchTimeout); this.batchTimeout = setTimeout(() => { // Update known hashes for minimal updates for (let model of this.bufferedUpdates) { - if (model.hash && model.hash === this.knownObjectHashes[model.name]) { + if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) { // Delete this useless update let foundFirst = false; this.bufferedUpdates = this.bufferedUpdates.filter(m => { diff --git a/frontend/viewer/ModelViewerWrapper.vue b/frontend/viewer/ModelViewerWrapper.vue index 18f2ad5..380222e 100644 --- a/frontend/viewer/ModelViewerWrapper.vue +++ b/frontend/viewer/ModelViewerWrapper.vue @@ -8,8 +8,8 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot"; import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; import type {Vector3} from "three"; -import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh'; import {BufferGeometry, Mesh} from "three"; +import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh'; ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking //@ts-ignore @@ -41,8 +41,32 @@ onMounted(() => { emit('load') }); elem.value.addEventListener('camera-change', onCameraChange); + elem.value.addEventListener('progress', + (ev) => onProgress((ev as ProgressEvent).detail.totalProgress)); }); +// Handles loading the events for 's slotted progress bar +const progressBar = ref(null); +const updateBar = ref(null); +let onProgressHideTimeout: number | null = null; +const onProgress = (totalProgress: number) => { + if (!progressBar.value || !updateBar.value) return; + // Update the progress bar and ensure it's visible + progressBar.value.style.display = 'block'; + progressBar.value.style.opacity = '1'; // Fade in + updateBar.value.style.width = `${totalProgress * 100}%`; + // Auto-hide smoothly when no progress is made for a while + if (onProgressHideTimeout) clearTimeout(onProgressHideTimeout); + onProgressHideTimeout = setTimeout(() => { + if (!progressBar.value) return; + progressBar.value.style.opacity = '0'; // Fade out + setTimeout(() => { + if (!progressBar.value) return; + progressBar.value.style.display = 'none'; // Actually hide + }, 300); // 0.3s fade out + }, 1000); +}; + class Line3DData { startHotspot: HTMLElement = document.body endHotspot: HTMLElement = document.body @@ -137,7 +161,7 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] { return Object.entries(lines); } -defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D}); +defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D, onProgress}); let {disableTap} = inject<{ disableTap: Ref }>('disableTap')!!; watch(disableTap, (value) => { @@ -155,7 +179,8 @@ watch(disableTap, (value) => { :shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background" :environment-image="settings.background"> - + +
Trying to load models from... @@ -163,6 +188,11 @@ watch(disableTap, (value) => {
+ + +
+
+
@@ -223,4 +253,30 @@ watch(disableTap, (value) => { .initial-load-banner .v-list-item { overflow: hidden; } + +.progress-bar { + display: block; + pointer-events: none; + width: 100%; + height: 10%; + max-height: 2%; + position: absolute; + left: 50%; + top: 0; + transform: translate3d(-50%, 0%, 0); + border-radius: 25px; + box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.5), 0 0 5px 1px rgba(0, 0, 0, 0.6); + border: 1px solid rgba(255, 255, 255, 0.9); + background-color: rgba(0, 0, 0, 0.5); + transition: opacity 0.3s; +} + +.update-bar { + background-color: rgba(255, 255, 255, 0.9); + width: 0; + height: 100%; + border-radius: 25px; + float: left; + transition: width 0.3s; +} \ No newline at end of file diff --git a/yacv_server/cad.py b/yacv_server/cad.py index 3cf78d9..c72d95f 100644 --- a/yacv_server/cad.py +++ b/yacv_server/cad.py @@ -46,16 +46,16 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]: return None -def grab_all_cad() -> List[Tuple[str, CADCoreLike]]: +def grab_all_cad() -> set[Tuple[str, CADCoreLike]]: """ Grab all shapes by inspecting the stack """ import inspect stack = inspect.stack() - shapes = [] + shapes = set() for frame in stack: for key, value in frame.frame.f_locals.items(): shape = get_shape(value, error=False) - if shape: - shapes.append((key, shape)) + if shape and shape not in shapes: + shapes.add((key, shape)) return shapes diff --git a/yacv_server/yacv.py b/yacv_server/yacv.py index 125d5e8..4a10405 100644 --- a/yacv_server/yacv.py +++ b/yacv_server/yacv.py @@ -194,7 +194,7 @@ class YACV: def show_cad_all(self, **kwargs): """Publishes all CAD objects in the current scope to the server""" - all_cad = grab_all_cad() + all_cad = list(grab_all_cad()) # List for reproducible iteration order self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs) def remove(self, name: str):