mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Add a progress bar to the frontend and improve slightly batched updates logic
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user