Minor fixes and drag and drop models onto interface

This commit is contained in:
Yeicor
2025-07-26 17:01:42 +02:00
parent 0855a9c6c7
commit c877fef490
9 changed files with 81 additions and 20 deletions

View File

@@ -23,9 +23,9 @@ in a web browser.
The [example](example) is a fully working project that shows how to use the viewer. The [example](example) is a fully working project that shows how to use the viewer.
You can play with the latest You can play with the latest
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb) demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
(or (or
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)). [without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
![Demo](assets/screenshot.png) ![Demo](assets/screenshot.png)

View File

@@ -108,6 +108,23 @@ 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:"); 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:");
if (modelUrl) await networkMgr.load(modelUrl); if (modelUrl) await networkMgr.load(modelUrl);
} }
// Detect dropped .glb files and load them manually
document.body.addEventListener("dragover", e => {
e.preventDefault(); // Allow drop
});
document.body.addEventListener("drop", async e => {
e.preventDefault();
const file = e.dataTransfer?.files?.[0];
if (!file) return;
const ext = file.name.split('.').pop()?.toLowerCase();
if (ext === 'glb' || ext === 'gltf') {
await networkMgr.load(file);
}
});
</script> </script>
<template> <template>

View File

@@ -15,11 +15,11 @@ let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_B
* *
* Remember to call mergeFinalize after all models have been merged (slower required operations). * Remember to call mergeFinalize after all models have been merged (slower required operations).
*/ */
export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => { export async function mergePartial(url: string | Blob, name: string, document: Document, networkFinished: () => void = () => {
}): Promise<Document> { }): Promise<Document> {
// Fetch the complete document from the network // Fetch the complete document from the network
// This could be done at the same time as the document is being processed, but I wanted better metrics // This could be done at the same time as the document is being processed, but I wanted better metrics
let response = await fetch(url); let response = await fetchOrRead(url);
let buffer = await response.arrayBuffer(); let buffer = await response.arrayBuffer();
networkFinished(); networkFinished();
@@ -118,3 +118,30 @@ function mergeScenes(): Transform {
} }
} }
} }
/** Fetches a URL or reads it if it is a Blob URL */
async function fetchOrRead(url: string | Blob) {
if (url instanceof Blob) {
// Use the FileReader API as fetch does not support Blob URLs
return new Promise<Response>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = (event: ProgressEvent<FileReader>) => {
if (event.target && event.target.result) {
resolve(new Response(event.target.result));
} else {
reject(new Error("Failed to read Blob URL: " + url));
}
};
reader.onerror = (error) => {
reject(new Error("Error reading Blob URL: " + url + " - " + error));
};
// Read the Blob URL as an ArrayBuffer
reader.readAsArrayBuffer(new Blob([url]));
});
} else {
// Fetch the URL
return fetch(url);
}
}

View File

@@ -4,12 +4,12 @@ const batchTimeout = 250; // ms
export class NetworkUpdateEventModel { export class NetworkUpdateEventModel {
name: string; name: string;
url: string; url: string | Blob;
// TODO: Detect and manage instances of the same object (same hash, different name) // TODO: Detect and manage instances of the same object (same hash, different name)
hash: string | null; hash: string | null;
isRemove: boolean | null; // This is null for a shutdown event isRemove: boolean | null; // This is null for a shutdown event
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) { constructor(name: string, url: string | Blob, hash: string | null, isRemove: boolean | null) {
this.name = name; this.name = name;
this.url = url; this.url = url;
this.hash = hash; this.hash = hash;
@@ -42,18 +42,26 @@ export class NetworkManager extends EventTarget {
* *
* Updates will be emitted as "update" events, including the download URL and the model name. * Updates will be emitted as "update" events, including the download URL and the model name.
*/ */
async load(url: string) { async load(url: string | Blob) {
if (url.startsWith("dev+") || url.startsWith("dev ")) { if (url instanceof String && (url.startsWith("dev+") || url.startsWith("dev "))) {
let baseUrl = new URL(url.slice(4)); let baseUrl = new URL(url.slice(4));
baseUrl.searchParams.set("api_updates", "true"); baseUrl.searchParams.set("api_updates", "true");
await this.monitorDevServer(baseUrl); await this.monitorDevServer(baseUrl);
} else { } else {
// Get the last part of the URL as the "name" of the model let name;
let name = url.split("/").pop(); let hash = null;
name = name?.split(".")[0] || `unknown-${Math.random()}`; if (url instanceof Blob) {
// Use a head request to get the hash of the file if (url instanceof File) name = (url as File).name
let response = await fetch(url, {method: "HEAD"}); else name = `blob-${Math.random()}`;
let hash = response.headers.get("etag"); name = name.replace('.glb', '').replace('.gltf', '');
} else {
// Get the last part of the URL as the "name" of the model
name = url.split("/").pop();
name = name?.split(".")[0] || `unknown-${Math.random()}`;
// Use a head request to get the hash of the file
let response = await fetch(url, {method: "HEAD"});
hash = response.headers.get("etag");
}
// Only trigger an update if the hash has changed // Only trigger an update if the hash has changed
this.foundModel(name, hash, url, false); this.foundModel(name, hash, url, false);
} }
@@ -92,7 +100,7 @@ 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 | Blob, isRemove: boolean | null, disconnect: () => void = () => {
}) { }) {
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove); // console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);

View File

@@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js"
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */ /** This class helps manage SceneManagerData. All methods are static to support reactivity... */
export class SceneMgr { export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */ /** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> { static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string | Blob, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
let loadStart = performance.now(); let loadStart = performance.now();
let loadNetworkEnd: number; let loadNetworkEnd: number;
@@ -100,7 +100,9 @@ export class SceneMgr {
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform); newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform); newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)])); let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene); let newDocument = await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
URL.revokeObjectURL(helpersUrl);
return newDocument;
} }
/** Serializes the current document into a GLB and updates the viewerSrc */ /** Serializes the current document into a GLB and updates the viewerSrc */
@@ -112,6 +114,7 @@ export class SceneMgr {
let buffer = await toBuffer(document); let buffer = await toBuffer(document);
let blob = new Blob([buffer], {type: 'model/gltf-binary'}); let blob = new Blob([buffer], {type: 'model/gltf-binary'});
console.debug("Showing current doc", document, "with", buffer.length, "total bytes"); console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
if (sceneUrl.value.startsWith("blob:")) URL.revokeObjectURL(sceneUrl.value);
sceneUrl.value = URL.createObjectURL(blob); sceneUrl.value = URL.createObjectURL(blob);
return document; return document;

View File

@@ -2,6 +2,7 @@
import {ungzip} from "pako"; import {ungzip} from "pako";
import {b64UrlDecode} from "../tools/b64.ts"; import {b64UrlDecode} from "../tools/b64.ts";
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
export const settings = (async () => { export const settings = (async () => {
let settings = { let settings = {
preload: [ preload: [
@@ -77,6 +78,12 @@ export const settings = (async () => {
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible) // 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++) { for (let i = 0; i < settings.preload.length; i++) {
let url = settings.preload[i]; let url = settings.preload[i];
// Ignore empty preload URLs to allow overriding default auto behavior
if (url === '') {
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
continue; // Skip this preload URL
}
// Handle special <auto> preload URL
if (url === '<auto>') { if (url === '<auto>') {
if (settings.pg_code != "") { // <auto> means no preload URL if code is set if (settings.pg_code != "") { // <auto> means no preload URL if code is set
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1)); settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
@@ -112,7 +119,6 @@ export const settings = (async () => {
return settings; return settings;
})() })()
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
function parseSetting(name: string, value: string, settings: any): any { function parseSetting(name: string, value: string, settings: any): any {
let arrayElem = name.endsWith(".0") let arrayElem = name.endsWith(".0")
if (arrayElem) name = name.slice(0, -2); if (arrayElem) name = name.slice(0, -2);

View File

@@ -141,7 +141,7 @@ function onModelData(modelData: string) {
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString()); "Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
// - Create a Blob from the binary data to be used as a URL // - Create a Blob from the binary data to be used as a URL
const blob = new Blob([binaryData], {type: 'model/gltf-binary'}); const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
} }
// - Emit the event with the model metadata and URL // - Emit the event with the model metadata and URL
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => { let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {

View File

@@ -128,6 +128,7 @@ async function downloadSceneGlb() {
link.download = file.name; link.download = file.name;
link.href = URL.createObjectURL(file); link.href = URL.createObjectURL(file);
link.click(); link.click();
URL.revokeObjectURL(link.href);
} }
async function openGithub() { async function openGithub() {

View File

@@ -141,7 +141,6 @@ class YACV:
"""Initializes the YACV server""" """Initializes the YACV server"""
raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper() raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper()
self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP
self.protocol = YACVProtocol.STDERR
self.server_thread = None self.server_thread = None
self.server = None self.server = None
self.startup_complete = threading.Event() self.startup_complete = threading.Event()