mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Minor fixes and drag and drop models onto interface
This commit is contained in:
@@ -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)).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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], () => {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user