Add a progress bar to the frontend and improve slightly batched updates logic

This commit is contained in:
Yeicor
2024-03-28 22:52:34 +01:00
parent 9afa2e5786
commit 4b06559ab8
5 changed files with 78 additions and 14 deletions

View File

@@ -33,6 +33,10 @@ const setDisableTap = (val: boolean) => disableTap.value = val;
provide('disableTap', {disableTap, setDisableTap}); provide('disableTap', {disableTap, setDisableTap});
async function onModelUpdateRequest(event: NetworkUpdateEvent) { 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 // Load/unload a new batch of models to optimize rendering time
console.log("Received model update request", event.models); console.log("Received model update request", event.models);
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null); 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 // Set up the load model event listener
let networkMgr = new NetworkManager(); let networkMgr = new NetworkManager();
networkMgr.addEventListener('update-early',
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent)); networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
// Start loading all configured models ASAP // Start loading all configured models ASAP
for (let model of settings.preload) { for (let model of settings.preload) {

View File

@@ -93,20 +93,22 @@ export class NetworkManager extends EventTarget {
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => { 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); console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
// Also update buffered updates to have only the latest one per model
// We only care about the latest update per model name
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name); this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
// Add the new model to the list of updates // Add the new model to the list of updates and dispatch the early update
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove); let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
this.bufferedUpdates.push(newModel); this.bufferedUpdates.push(upd);
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
// Optimization: try to batch updates automatically for faster rendering // Optimization: try to batch updates automatically for faster rendering
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout); if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
this.batchTimeout = setTimeout(() => { this.batchTimeout = setTimeout(() => {
// Update known hashes for minimal updates // Update known hashes for minimal updates
for (let model of this.bufferedUpdates) { 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 // Delete this useless update
let foundFirst = false; let foundFirst = false;
this.bufferedUpdates = this.bufferedUpdates.filter(m => { this.bufferedUpdates = this.bufferedUpdates.filter(m => {

View File

@@ -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 {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
import type {Vector3} from "three"; import type {Vector3} from "three";
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
import {BufferGeometry, Mesh} from "three"; import {BufferGeometry, Mesh} from "three";
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
//@ts-ignore //@ts-ignore
@@ -41,8 +41,32 @@ onMounted(() => {
emit('load') emit('load')
}); });
elem.value.addEventListener('camera-change', onCameraChange); elem.value.addEventListener('camera-change', onCameraChange);
elem.value.addEventListener('progress',
(ev) => onProgress((ev as ProgressEvent).detail.totalProgress));
}); });
// Handles loading the events for <model-viewer>'s slotted progress bar
const progressBar = ref<HTMLElement | null>(null);
const updateBar = ref<HTMLElement | null>(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 { class Line3DData {
startHotspot: HTMLElement = document.body startHotspot: HTMLElement = document.body
endHotspot: HTMLElement = document.body endHotspot: HTMLElement = document.body
@@ -137,7 +161,7 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
return Object.entries(lines); return Object.entries(lines);
} }
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D}); defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D, onProgress});
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!; let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
watch(disableTap, (value) => { watch(disableTap, (value) => {
@@ -155,7 +179,8 @@ watch(disableTap, (value) => {
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
:environment-image="settings.background"> :environment-image="settings.background">
<slot></slot> <!-- Controls, annotations, etc. --> <slot></slot>
<!-- Display some information during initial load -->
<div class="annotation initial-load-banner"> <div class="annotation initial-load-banner">
Trying to load models from... Trying to load models from...
<v-list v-for="src in settings.preload" :key="src"> <v-list v-for="src in settings.preload" :key="src">
@@ -163,6 +188,11 @@ watch(disableTap, (value) => {
</v-list> </v-list>
<!-- Too much idle CPU usage: <loading></loading> --> <!-- Too much idle CPU usage: <loading></loading> -->
</div> </div>
<!-- Customize the progress bar -->
<div class="progress-bar" slot="progress-bar" ref="progressBar">
<div class="update-bar" ref="updateBar"/>
</div>
</model-viewer> </model-viewer>
<!-- The SVG overlay for fake 3D lines attached to the model --> <!-- The SVG overlay for fake 3D lines attached to the model -->
@@ -223,4 +253,30 @@ watch(disableTap, (value) => {
.initial-load-banner .v-list-item { .initial-load-banner .v-list-item {
overflow: hidden; 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;
}
</style> </style>

View File

@@ -46,16 +46,16 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
return None 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 """ """ Grab all shapes by inspecting the stack """
import inspect import inspect
stack = inspect.stack() stack = inspect.stack()
shapes = [] shapes = set()
for frame in stack: for frame in stack:
for key, value in frame.frame.f_locals.items(): for key, value in frame.frame.f_locals.items():
shape = get_shape(value, error=False) shape = get_shape(value, error=False)
if shape: if shape and shape not in shapes:
shapes.append((key, shape)) shapes.add((key, shape))
return shapes return shapes

View File

@@ -194,7 +194,7 @@ class YACV:
def show_cad_all(self, **kwargs): def show_cad_all(self, **kwargs):
"""Publishes all CAD objects in the current scope to the server""" """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) self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
def remove(self, name: str): def remove(self, name: str):