mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 14:14:13 +01:00
Minor fixes and drag and drop models onto interface
This commit is contained in:
@@ -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:");
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
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> {
|
||||
// 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
|
||||
let response = await fetch(url);
|
||||
let response = await fetchOrRead(url);
|
||||
let buffer = await response.arrayBuffer();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ const batchTimeout = 250; // ms
|
||||
|
||||
export class NetworkUpdateEventModel {
|
||||
name: string;
|
||||
url: string;
|
||||
url: string | Blob;
|
||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||
hash: string | null;
|
||||
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.url = url;
|
||||
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.
|
||||
*/
|
||||
async load(url: string) {
|
||||
if (url.startsWith("dev+") || url.startsWith("dev ")) {
|
||||
async load(url: string | Blob) {
|
||||
if (url instanceof String && (url.startsWith("dev+") || url.startsWith("dev "))) {
|
||||
let baseUrl = new URL(url.slice(4));
|
||||
baseUrl.searchParams.set("api_updates", "true");
|
||||
await this.monitorDevServer(baseUrl);
|
||||
} else {
|
||||
// Get the last part of the URL as the "name" of the model
|
||||
let 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"});
|
||||
let hash = response.headers.get("etag");
|
||||
let name;
|
||||
let hash = null;
|
||||
if (url instanceof Blob) {
|
||||
if (url instanceof File) name = (url as File).name
|
||||
else name = `blob-${Math.random()}`;
|
||||
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
|
||||
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);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js"
|
||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||
export class SceneMgr {
|
||||
/** 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 loadNetworkEnd: number;
|
||||
|
||||
@@ -100,7 +100,9 @@ export class SceneMgr {
|
||||
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
|
||||
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
|
||||
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 */
|
||||
@@ -112,6 +114,7 @@ export class SceneMgr {
|
||||
let buffer = await toBuffer(document);
|
||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||
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);
|
||||
|
||||
return document;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {ungzip} from "pako";
|
||||
import {b64UrlDecode} from "../tools/b64.ts";
|
||||
|
||||
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
|
||||
export const settings = (async () => {
|
||||
let settings = {
|
||||
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)
|
||||
for (let i = 0; i < settings.preload.length; 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 (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));
|
||||
@@ -112,7 +119,6 @@ export const settings = (async () => {
|
||||
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 {
|
||||
let arrayElem = name.endsWith(".0")
|
||||
if (arrayElem) name = name.slice(0, -2);
|
||||
|
||||
@@ -141,7 +141,7 @@ function onModelData(modelData: string) {
|
||||
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
|
||||
// - Create a Blob from the binary data to be used as a URL
|
||||
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
|
||||
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
|
||||
|
||||
@@ -128,6 +128,7 @@ async function downloadSceneGlb() {
|
||||
link.download = file.name;
|
||||
link.href = URL.createObjectURL(file);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
async function openGithub() {
|
||||
|
||||
Reference in New Issue
Block a user