"Proper" top-level-await workaround

This commit is contained in:
Yeicor
2025-01-04 23:22:18 +01:00
parent b09d5a87fa
commit 8330060bd9
10 changed files with 125 additions and 221 deletions

View File

@@ -52,10 +52,11 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
let model = event.models[modelIndex];
tools.value?.removeObjectSelections(model.name);
try {
let loadHelpers = (await settings()).loadHelpers;
if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && loadHelpers, isLast);
} else {
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && loadHelpers, isLast);
}
} catch (e) {
console.error("Error loading model", model, e);
@@ -79,16 +80,18 @@ 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));
// Start loading all configured models ASAP
for (let model of settings.preload) {
networkMgr.load(model);
}
watch(viewer, (newViewer) => {
if (newViewer) {
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
' models from:</tspan>' + settings.preload.map((url) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
(async () => { // Start loading all configured models ASAP
let sett = await settings();
watch(viewer, (newViewer) => {
if (newViewer) {
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
' models from:</tspan>' + sett.preload.map((url: string) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
}
});
for (let model of sett.preload) {
await networkMgr.load(model);
}
});
})();
async function loadModelManual() {
const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:");

View File

@@ -61,6 +61,7 @@ export class NetworkManager extends EventTarget {
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
while (!stop()) {
let monitorEveryMs = (await settings()).monitorEveryMs;
try {
// WARNING: This will spam the console logs with failed requests when the server is down
const controller = new AbortController();
@@ -82,12 +83,12 @@ export class NetworkManager extends EventTarget {
}
} else {
// Server is down, wait a little longer before retrying
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
await new Promise(resolve => setTimeout(resolve, 10 * monitorEveryMs));
}
controller.abort();
} catch (e) { // Ignore errors (retry very soon)
}
await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
await new Promise(resolve => setTimeout(resolve, monitorEveryMs));
}
}

View File

@@ -1,35 +1,66 @@
// These are the default values for the settings, which are overridden below
export const settings = {
preload: [
// @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
// Websocket URLs automatically listen for new models from the python backend
'<auto>', // Get the default preload URL if not overridden
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,
// ModelViewer settings
autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1,
shadowIntensity: 0,
background: '',
let settingsCache: any = null;
export async function settings() {
if (settingsCache !== null) return settingsCache;
let settings = {
preload: [
// @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
// Websocket URLs automatically listen for new models from the python backend
'<auto>', // Get the default preload URL if not overridden
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,
// ModelViewer settings
autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1,
shadowIntensity: 0,
background: '',
};
// Auto-override any settings from the URL
const url = new URL(window.location.href);
url.searchParams.forEach((value, key) => {
if (key in settings) (settings as any)[key] = parseSetting(key, value, settings);
})
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
for (let i = 0; i < settings.preload.length; i++) {
let url = settings.preload[i];
if (url === '<auto>') {
const possibleBackend = new URL("./?api_updates=true", window.location.href)
await fetch(possibleBackend, {method: "HEAD"}).then((response) => {
if (response.ok && response.headers.get("Content-Type") === "text/event-stream") {
// Frontend served by the backend: default to this URL for updates
url = "dev+" + possibleBackend.href;
}
}).catch((error) => console.error("Failed to check for backend:", error));
if (url === '<auto>') { // Fallback to the default preload URL of localhost
url = "dev+http://localhost:32323";
}
}
settings.preload[i] = url;
}
settingsCache = settings;
return settings;
}
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
function parseSetting(name: string, value: string): any {
function parseSetting(name: string, value: string, settings: any): any {
let arrayElem = name.endsWith(".0")
if (arrayElem) name = name.slice(0, -2);
let prevValue = (settings as any)[name];
@@ -42,7 +73,7 @@ function parseSetting(name: string, value: string): any {
} else {
toExtend = prevValue;
}
toExtend.push(parseSetting(name + ".0", value));
toExtend.push(parseSetting(name + ".0", value, settings));
return toExtend;
} else {
prevValue = prevValue[0];
@@ -59,27 +90,3 @@ function parseSetting(name: string, value: string): any {
throw new Error(`Unknown setting type: ${typeof prevValue} -- ${prevValue}`);
}
}
// Auto-override any settings from the URL
const url = new URL(window.location.href);
url.searchParams.forEach((value, key) => {
if (key in settings) (settings as any)[key] = parseSetting(key, value);
})
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
for (let i = 0; i < settings.preload.length; i++) {
let url = settings.preload[i];
if (url === '<auto>') {
const possibleBackend = new URL("./?api_updates=true", window.location.href)
await fetch(possibleBackend, {method: "HEAD"}).then((response) => {
if (response.ok && response.headers.get("Content-Type") === "text/event-stream") {
// Frontend served by the backend: default to this URL for updates
url = "dev+" + possibleBackend.href;
}
}).catch((error) => console.error("Failed to check for backend:", error));
if (url === '<auto>') { // Fallback to the default preload URL of localhost
url = "dev+http://localhost:32323";
}
}
settings.preload[i] = url;
}

View File

@@ -55,7 +55,11 @@ const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(settings.edgeWidth);
const edgeWidth = ref(0);
(async () => {
let s = await settings();
edgeWidth.value = s.edgeWidth;
})();
// Misc properties
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
import {inject, ref, type ShallowRef, watch} from "vue";
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
import SvgIcon from '@jamescoyle/vue-icon';
import type {ModelViewerElement} from '@google/model-viewer';
@@ -333,7 +333,7 @@ function updateBoundingBox() {
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
edge = axisEdges[0];
let edgeDist = Infinity;
let cameraPos: Vector3 = props.viewer?.scene.camera.position;
let cameraPos: Vector3 = props.viewer?.scene?.camera?.position ?? new Vector3();
for (let testEdge of axisEdges) {
let from = new Vector3(...corners[testEdge[0]]);
let to = new Vector3(...corners[testEdge[1]]);

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import {settings} from "../misc/settings";
import {inject, onMounted, type Ref, ref, watch} from "vue";
import {inject, onUpdated, type Ref, ref, watch} from "vue";
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
import {$controls} from '@google/model-viewer/lib/features/controls.js';
import {type SmoothControls} from '@google/model-viewer/lib/three-components/SmoothControls';
@@ -27,14 +27,19 @@ const scene = ref<ModelScene | null>(null);
const renderer = ref<Renderer | null>(null);
const controls = ref<SmoothControls | null>(null);
const sett = ref<any | null>(null);
(async () => sett.value = await settings())();
let lastCameraTargetPosition: Vector3 | undefined = undefined;
let lastCameraZoom: number | undefined = undefined;
let lastCameraUrl = props.src.toString();
onMounted(() => {
if (!elem.value) return;
let initialized = false
onUpdated(() => {
if (!elem.value) return; // Not ready yet
if (initialized) return; // Already initialized
initialized = true;
elem.value.addEventListener('before-render', () => {
if (!elem.value) return;
if (!elem.value) return
// Extract internals of model-viewer in order to hack unsupported features
scene.value = elem.value[$scene] as ModelScene;
renderer.value = elem.value[$renderer] as Renderer;
@@ -207,11 +212,11 @@ watch(disableTap, (newDisableTap) => {
<template>
<!-- The main 3D model viewer -->
<model-viewer ref="elem" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :autoplay="settings.autoplay"
:environment-image="settings.background" :exposure="settings.exposure"
:orbit-sensitivity="settings.orbitSensitivity" :pan-sensitivity="settings.panSensitivity"
:poster="poster" :shadow-intensity="settings.shadowIntensity" :skybox-image="settings.background"
:src="props.src" :zoom-sensitivity="settings.zoomSensitivity" alt="The 3D model(s)" camera-controls
<model-viewer ref="elem" v-if="sett != null" :ar="sett.arModes.length > 0" :ar-modes="sett.arModes"
:environment-image="sett.background" :exposure="sett.exposure" :autoplay="sett.autoplay"
:orbit-sensitivity="sett.orbitSensitivity" :pan-sensitivity="sett.panSensitivity"
:poster="poster" :shadow-intensity="sett.shadowIntensity" :skybox-image="sett.background"
:src="props.src" :zoom-sensitivity="sett.zoomSensitivity" alt="The 3D model(s)" camera-controls
camera-orbit="30deg 75deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
min-camera-orbit="-Infinity 0deg 5%" style="width: 100%; height: 100%">
<slot></slot>