mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 06:27:04 +01:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f30c2fd0a | ||
|
|
ba05a8072b | ||
|
|
27f540be23 | ||
|
|
e8c0f683c5 | ||
|
|
345636e478 | ||
|
|
9a0fb03526 | ||
|
|
2037621afc | ||
|
|
2ba0e18479 | ||
|
|
eca2bbfa7c | ||
|
|
86180c424e | ||
|
|
e42d6be074 | ||
|
|
e2d6a3cb00 | ||
|
|
9e453b7890 | ||
|
|
0b8faa9e8b | ||
|
|
00bc2a15e0 | ||
|
|
432abcf85c | ||
|
|
4b6d3f6266 | ||
|
|
255ae72ed2 | ||
|
|
77dd9fb43e | ||
|
|
5dc2ae2f8d | ||
|
|
58440723bd | ||
|
|
bfdd656316 | ||
|
|
7408823c02 | ||
|
|
856ffbc4c5 | ||
|
|
d0f8463bbf | ||
|
|
162d3e22a2 | ||
|
|
4b06559ab8 | ||
|
|
9afa2e5786 |
2
.github/workflows/deploy2.yml
vendored
2
.github/workflows/deploy2.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
mv "$dir/"* public/
|
mv "$dir/"* public/
|
||||||
rmdir "$dir"
|
rmdir "$dir"
|
||||||
done
|
done
|
||||||
- uses: "actions/configure-pages@v4"
|
- uses: "actions/configure-pages@v5"
|
||||||
- uses: "actions/upload-pages-artifact@v3"
|
- uses: "actions/upload-pages-artifact@v3"
|
||||||
with:
|
with:
|
||||||
path: 'public'
|
path: 'public'
|
||||||
|
|||||||
@@ -2225,13 +2225,13 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- three@0.162.0
|
- three@0.160.1
|
||||||
|
|
||||||
This package contains the following license and notice below:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
The MIT License
|
The MIT License
|
||||||
|
|
||||||
Copyright © 2010-2024 three.js authors
|
Copyright © 2010-2023 three.js authors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -2439,7 +2439,7 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- vuetify@3.5.11
|
- vuetify@3.5.13
|
||||||
|
|
||||||
This package contains the following license and notice below:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<!--suppress SillyAssignmentJS -->
|
<!--suppress SillyAssignmentJS -->
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef} from "vue";
|
import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef, watch} from "vue";
|
||||||
import Sidebar from "./misc/Sidebar.vue";
|
import Sidebar from "./misc/Sidebar.vue";
|
||||||
import Loading from "./misc/Loading.vue";
|
import Loading from "./misc/Loading.vue";
|
||||||
import Tools from "./tools/Tools.vue";
|
import Tools from "./tools/Tools.vue";
|
||||||
import Models from "./models/Models.vue";
|
import Models from "./models/Models.vue";
|
||||||
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
|
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
|
||||||
import {settings} from "./misc/settings";
|
import {settings} from "./misc/settings";
|
||||||
import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
|
import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
|
||||||
import {SceneMgr} from "./misc/scene";
|
import {SceneMgr} from "./misc/scene";
|
||||||
import {Document} from "@gltf-transform/core";
|
import {Document} from "@gltf-transform/core";
|
||||||
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
|
||||||
@@ -28,11 +28,16 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
|||||||
const sceneDocument = shallowRef(new Document());
|
const sceneDocument = shallowRef(new Document());
|
||||||
provide('sceneDocument', {sceneDocument});
|
provide('sceneDocument', {sceneDocument});
|
||||||
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
||||||
|
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
|
||||||
const disableTap = ref(false);
|
const disableTap = ref(false);
|
||||||
const setDisableTap = (val: boolean) => disableTap.value = val;
|
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);
|
||||||
@@ -45,6 +50,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
|||||||
for (let modelIndex in event.models) {
|
for (let modelIndex in event.models) {
|
||||||
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
||||||
let model = event.models[modelIndex];
|
let model = event.models[modelIndex];
|
||||||
|
tools.value?.removeObjectSelections(model.name);
|
||||||
try {
|
try {
|
||||||
if (!model.isRemove) {
|
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 && settings.loadHelpers, isLast);
|
||||||
@@ -64,17 +70,25 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onModelRemoveRequest(name: string) {
|
async function onModelRemoveRequest(name: string) {
|
||||||
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
|
await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
|
||||||
triggerRef(sceneDocument); // Why not triggered automatically?
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
networkMgr.load(model);
|
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 function loadModelManual() {
|
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:");
|
||||||
@@ -91,24 +105,24 @@ async function loadModelManual() {
|
|||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<!-- The left collapsible sidebar has the list of models -->
|
<!-- The left collapsible sidebar has the list of models -->
|
||||||
<sidebar :opened-init="openSidebarsByDefault" side="left" :width="300">
|
<sidebar :opened-init="openSidebarsByDefault" :width="300" side="left">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<v-toolbar-title>Models</v-toolbar-title>
|
<v-toolbar-title>Models</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
<template #toolbar-items>
|
<template #toolbar-items>
|
||||||
<v-btn icon="" @click="loadModelManual">
|
<v-btn icon="" @click="loadModelManual">
|
||||||
<svg-icon type="mdi" :path="mdiPlus"/>
|
<svg-icon :path="mdiPlus" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
|
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
|
||||||
</sidebar>
|
</sidebar>
|
||||||
|
|
||||||
<!-- The right collapsible sidebar has the list of tools -->
|
<!-- The right collapsible sidebar has the list of tools -->
|
||||||
<sidebar :opened-init="openSidebarsByDefault" side="right" :width="48 * 3 /* buttons */ + 1 /* border? */">
|
<sidebar :opened-init="openSidebarsByDefault" :width="48 * 3 /* buttons */ + 1 /* border? */" side="right">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<v-toolbar-title>Tools</v-toolbar-title>
|
<v-toolbar-title>Tools</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||||
</sidebar>
|
</sidebar>
|
||||||
|
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components/index.mjs";
|
import {VCol, VContainer, VProgressCircular, VRow} from "vuetify/lib/components/index.mjs";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
|
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
|
||||||
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
|
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
|
||||||
@@ -16,22 +16,22 @@ const openIcon = props.side === 'left' ? mdiChevronRight : mdiChevronLeft;
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-btn icon @click="opened = !opened" class="open-button" :class="side">
|
<v-btn :class="side" class="open-button" icon @click="opened = !opened">
|
||||||
<svg-icon type="mdi" :path="openIcon"/>
|
<svg-icon :path="openIcon" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width">
|
<v-navigation-drawer v-model="opened" :location="side" :width="props.width" permanent>
|
||||||
<v-toolbar density="compact">
|
<v-toolbar density="compact">
|
||||||
<v-toolbar-items v-if="side == 'right'">
|
<v-toolbar-items v-if="side == 'right'">
|
||||||
<slot name="toolbar-items"></slot>
|
<slot name="toolbar-items"></slot>
|
||||||
<v-btn icon @click="opened = !opened">
|
<v-btn icon @click="opened = !opened">
|
||||||
<svg-icon type="mdi" :path="mdiClose"/>
|
<svg-icon :path="mdiClose" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar-items>
|
</v-toolbar-items>
|
||||||
<slot name="toolbar"></slot>
|
<slot name="toolbar"></slot>
|
||||||
<v-toolbar-items v-if="side == 'left'">
|
<v-toolbar-items v-if="side == 'left'">
|
||||||
<slot name="toolbar-items"></slot>
|
<slot name="toolbar-items"></slot>
|
||||||
<v-btn icon @click="opened = !opened">
|
<v-btn icon @click="opened = !opened">
|
||||||
<svg-icon type="mdi" :path="mdiClose"/>
|
<svg-icon :path="mdiClose" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar-items>
|
</v-toolbar-items>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
|
|||||||
center: Vector3,
|
center: Vector3,
|
||||||
vertices: Array<Vector3>
|
vertices: Array<Vector3>
|
||||||
} {
|
} {
|
||||||
selInfo.object.updateMatrixWorld();
|
|
||||||
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
||||||
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
||||||
if (ind === null) {
|
if (ind === null) {
|
||||||
|
|||||||
@@ -21,7 +21,32 @@ export async function mergePartial(url: string, name: string, document: Document
|
|||||||
networkFinished();
|
networkFinished();
|
||||||
|
|
||||||
// Load the new document
|
// Load the new document
|
||||||
let newDoc = await io.readBinary(new Uint8Array(buffer));
|
let newDoc = null;
|
||||||
|
let alreadyTried: { [name: string]: boolean } = {}
|
||||||
|
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
|
||||||
|
try { // Try to load fast if no extensions are used
|
||||||
|
newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||||
|
} catch (e) { // Fallback to wait for download and register big extensions
|
||||||
|
if (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||||
|
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
|
||||||
|
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||||
|
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
|
||||||
|
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||||
|
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||||
|
io.registerExtensions([KHRDracoMeshCompression])
|
||||||
|
.registerDependencies({
|
||||||
|
'draco3d.decoder': await dracoDecoderWeb.default({}),
|
||||||
|
'draco3d.encoder': await dracoEncoderWeb.default({})
|
||||||
|
});
|
||||||
|
} else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||||
|
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
|
||||||
|
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
|
||||||
|
io.registerExtensions([EXTTextureWebP]);
|
||||||
|
} else { // TODO: Add more extensions as required
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any previous model with the same name
|
// Remove any previous model with the same name
|
||||||
await document.transform(dropByName(name));
|
await document.transform(dropByName(name));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {settings} from "./settings";
|
|||||||
|
|
||||||
const batchTimeout = 250; // ms
|
const batchTimeout = 250; // ms
|
||||||
|
|
||||||
class NetworkUpdateEventModel {
|
export class NetworkUpdateEventModel {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
// 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)
|
||||||
@@ -93,28 +93,47 @@ 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 = () => {
|
||||||
}) {
|
}) {
|
||||||
let prevHash = this.knownObjectHashes[name];
|
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||||
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash);
|
|
||||||
if (!hash || hash !== prevHash || isRemove) {
|
|
||||||
// Update known hashes
|
|
||||||
if (isRemove == false) {
|
|
||||||
this.knownObjectHashes[name] = hash;
|
|
||||||
} else if (isRemove == true) {
|
|
||||||
if (!(name in this.knownObjectHashes)) return; // Nothing to remove...
|
|
||||||
delete this.knownObjectHashes[name];
|
|
||||||
// Also update buffered updates if the model is removed
|
|
||||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
|
||||||
}
|
|
||||||
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
|
||||||
this.bufferedUpdates.push(newModel);
|
|
||||||
|
|
||||||
// Optimization: try to batch updates automatically for faster rendering
|
// We only care about the latest update per model name
|
||||||
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
||||||
this.batchTimeout = setTimeout(() => {
|
|
||||||
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
// Add the new model to the list of updates and dispatch the early update
|
||||||
this.bufferedUpdates = [];
|
let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||||
}, batchTimeout);
|
this.bufferedUpdates.push(upd);
|
||||||
}
|
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
|
||||||
|
|
||||||
|
// Optimization: try to batch updates automatically for faster rendering
|
||||||
|
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
||||||
|
this.batchTimeout = setTimeout(() => {
|
||||||
|
// Update known hashes for minimal updates
|
||||||
|
for (let model of this.bufferedUpdates) {
|
||||||
|
if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) {
|
||||||
|
// Delete this useless update
|
||||||
|
let foundFirst = false;
|
||||||
|
this.bufferedUpdates = this.bufferedUpdates.filter(m => {
|
||||||
|
if (m === model) {
|
||||||
|
if (!foundFirst) { // Remove only first full match
|
||||||
|
foundFirst = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Keep this update and update the last known hash
|
||||||
|
if (model.isRemove == true) {
|
||||||
|
if (model.name in this.knownObjectHashes) delete this.knownObjectHashes[model.name];
|
||||||
|
} else if (model.isRemove == false) {
|
||||||
|
this.knownObjectHashes[model.name] = model.hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the event to actually update the models
|
||||||
|
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
||||||
|
this.bufferedUpdates = [];
|
||||||
|
}, batchTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,41 +13,30 @@ export class SceneMgr {
|
|||||||
let loadStart = performance.now();
|
let loadStart = performance.now();
|
||||||
let loadNetworkEnd: number;
|
let loadNetworkEnd: number;
|
||||||
|
|
||||||
// Start merging into the current document, replacing or adding as needed
|
try {
|
||||||
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
// Start merging into the current document, replacing or adding as needed
|
||||||
|
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
||||||
|
|
||||||
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
||||||
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
||||||
|
} finally {
|
||||||
|
if (updateHelpers) {
|
||||||
|
// Reload the helpers to fit the new model
|
||||||
|
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
||||||
|
reloadScene = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateHelpers) {
|
if (reloadScene) {
|
||||||
// Reload the helpers to fit the new model
|
// Display the final fully loaded model
|
||||||
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
let displayStart = performance.now();
|
||||||
reloadScene = false;
|
document = await this.showCurrentDoc(sceneUrl, document);
|
||||||
}
|
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
||||||
|
}
|
||||||
if (reloadScene) {
|
|
||||||
// Display the final fully loaded model
|
|
||||||
let displayStart = performance.now();
|
|
||||||
document = await this.showCurrentDoc(sceneUrl, document);
|
|
||||||
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> {
|
|
||||||
let bb = SceneMgr.getBoundingBox(document);
|
|
||||||
if (!bb) return document;
|
|
||||||
|
|
||||||
// Create the helper axes and grid box
|
|
||||||
let helpersDoc = new Document();
|
|
||||||
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getBoundingBox(document: Document): Box3 | null {
|
static getBoundingBox(document: Document): Box3 | null {
|
||||||
if (document.getRoot().listNodes().length === 0) return null;
|
if (document.getRoot().listNodes().length === 0) return null;
|
||||||
// Get bounding box of the model and use it to set the size of the helpers
|
// Get bounding box of the model and use it to set the size of the helpers
|
||||||
@@ -89,6 +78,19 @@ export class SceneMgr {
|
|||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> {
|
||||||
|
let bb = SceneMgr.getBoundingBox(document);
|
||||||
|
if (!bb) return document;
|
||||||
|
|
||||||
|
// Create the helper axes and grid box
|
||||||
|
let helpersDoc = new Document();
|
||||||
|
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/** Serializes the current document into a GLB and updates the viewerSrc */
|
/** Serializes the current document into a GLB and updates the viewerSrc */
|
||||||
private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
|
private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
|
||||||
// Make sure the document is fully loaded and ready to be shown
|
// Make sure the document is fully loaded and ready to be shown
|
||||||
@@ -97,7 +99,7 @@ export class SceneMgr {
|
|||||||
// Serialize the document into a GLB and update the viewerSrc
|
// Serialize the document into a GLB and update the viewerSrc
|
||||||
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, "as", Array.from(buffer));
|
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||||
sceneUrl.value = URL.createObjectURL(blob);
|
sceneUrl.value = URL.createObjectURL(blob);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ export const settings = {
|
|||||||
monitorEveryMs: 100,
|
monitorEveryMs: 100,
|
||||||
monitorOpenTimeoutMs: 1000,
|
monitorOpenTimeoutMs: 1000,
|
||||||
// ModelViewer settings
|
// ModelViewer settings
|
||||||
autoplay: true,
|
autoplay: true, // Global animation toggle
|
||||||
arModes: 'webxr scene-viewer quick-look',
|
arModes: 'webxr scene-viewer quick-look',
|
||||||
|
zoomSensitivity: 0.25,
|
||||||
|
orbitSensitivity: 1,
|
||||||
|
panSensitivity: 1,
|
||||||
exposure: 1,
|
exposure: 1,
|
||||||
shadowIntensity: 0,
|
shadowIntensity: 0,
|
||||||
background: '',
|
background: '',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
VBtn,
|
VBtn,
|
||||||
VBtnToggle,
|
VBtnToggle,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
VTooltip,
|
VTooltip,
|
||||||
} from "vuetify/lib/components/index.mjs";
|
} from "vuetify/lib/components/index.mjs";
|
||||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||||
import {Document, Mesh} from "@gltf-transform/core";
|
import {Mesh} from "@gltf-transform/core";
|
||||||
import {inject, ref, type ShallowRef, watch} from "vue";
|
import {ref, watch} from "vue";
|
||||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {
|
import {
|
||||||
mdiCircleOpacity,
|
mdiCircleOpacity,
|
||||||
@@ -33,7 +33,7 @@ import {Plane} from "three/src/math/Plane.js";
|
|||||||
import {Vector3} from "three/src/math/Vector3.js";
|
import {Vector3} from "three/src/math/Vector3.js";
|
||||||
import type {MObject3D} from "../tools/Selection.vue";
|
import type {MObject3D} from "../tools/Selection.vue";
|
||||||
import {toLineSegments} from "../misc/lines.js";
|
import {toLineSegments} from "../misc/lines.js";
|
||||||
import {settings} from "../misc/settings.js";
|
import {settings} from "../misc/settings.js"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
meshes: Array<Mesh>,
|
meshes: Array<Mesh>,
|
||||||
@@ -43,10 +43,11 @@ const emit = defineEmits<{ remove: [] }>()
|
|||||||
|
|
||||||
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||||
|
|
||||||
// Reactive properties
|
// Count the number of faces, edges and vertices
|
||||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
let faceCount = ref(-1);
|
||||||
const opacity = defineModel<number>("opacity", {default: 1});
|
let edgeCount = ref(-1);
|
||||||
const wireframe = ref(false);
|
let vertexCount = ref(-1);
|
||||||
|
|
||||||
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
||||||
const clipPlaneX = ref(1);
|
const clipPlaneX = ref(1);
|
||||||
const clipPlaneSwappedX = ref(false);
|
const clipPlaneSwappedX = ref(false);
|
||||||
@@ -56,24 +57,10 @@ const clipPlaneZ = ref(1);
|
|||||||
const clipPlaneSwappedZ = ref(false);
|
const clipPlaneSwappedZ = ref(false);
|
||||||
const edgeWidth = ref(settings.edgeWidth);
|
const edgeWidth = ref(settings.edgeWidth);
|
||||||
|
|
||||||
// Count the number of faces, edges and vertices
|
// Misc properties
|
||||||
let faceCount = props.meshes
|
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
const opacity = defineModel<number>("opacity", {default: 1});
|
||||||
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
const wireframe = ref(false);
|
||||||
.reduce((a, b) => a + b, 0)
|
|
||||||
let edgeCount = props.meshes
|
|
||||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
|
|
||||||
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
|
|
||||||
.reduce((a, b) => a + b, 0)
|
|
||||||
let vertexCount = props.meshes
|
|
||||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
|
|
||||||
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
|
|
||||||
.reduce((a, b) => a + b, 0)
|
|
||||||
|
|
||||||
// Set initial defaults for the enabled features
|
|
||||||
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
|
||||||
if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
|
||||||
if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
|
||||||
|
|
||||||
// Listeners for changes in the properties (or viewer reloads)
|
// Listeners for changes in the properties (or viewer reloads)
|
||||||
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||||
@@ -81,9 +68,6 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
if (!scene || !sceneModel) return;
|
||||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
|
||||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
|
||||||
// of not actually removing the primitives from the scene graph
|
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
||||||
@@ -101,16 +85,12 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
|||||||
scene.queueRender()
|
scene.queueRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(enabledFeatures, onEnabledFeaturesChange);
|
watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
|
||||||
|
|
||||||
function onOpacityChange(newOpacity: number) {
|
function onOpacityChange(newOpacity: number) {
|
||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
if (!scene || !sceneModel) return;
|
||||||
// Iterate all primitives of the mesh and set their opacity based on the enabled features
|
|
||||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
|
||||||
// of not actually removing the primitives from the scene graph
|
|
||||||
// console.log('Opacity may have changed', newOpacity)
|
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.material && child.material.opacity !== newOpacity) {
|
if (child.material && child.material.opacity !== newOpacity) {
|
||||||
@@ -129,10 +109,6 @@ function onWireframeChange(newWireframe: boolean) {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
if (!scene || !sceneModel) return;
|
||||||
// Iterate all primitives of the mesh and set their wireframe based on the enabled features
|
|
||||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
|
||||||
// of not actually removing the primitives from the scene graph
|
|
||||||
// console.log('Wireframe may have changed', newWireframe)
|
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.material && child.material.wireframe !== newWireframe) {
|
if (child.material && child.material.wireframe !== newWireframe) {
|
||||||
@@ -146,8 +122,6 @@ function onWireframeChange(newWireframe: boolean) {
|
|||||||
|
|
||||||
watch(wireframe, onWireframeChange);
|
watch(wireframe, onWireframeChange);
|
||||||
|
|
||||||
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
|
|
||||||
|
|
||||||
function onClipPlanesChange() {
|
function onClipPlanesChange() {
|
||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
@@ -204,8 +178,6 @@ watch(clipPlaneZ, onClipPlanesChange);
|
|||||||
watch(clipPlaneSwappedX, onClipPlanesChange);
|
watch(clipPlaneSwappedX, onClipPlanesChange);
|
||||||
watch(clipPlaneSwappedY, onClipPlanesChange);
|
watch(clipPlaneSwappedY, onClipPlanesChange);
|
||||||
watch(clipPlaneSwappedZ, onClipPlanesChange);
|
watch(clipPlaneSwappedZ, onClipPlanesChange);
|
||||||
// Clip planes are also affected by the camera position, so we need to listen to camera changes
|
|
||||||
props.viewer!!.onElemReady((elem) => elem.addEventListener('camera-change', onClipPlanesChange))
|
|
||||||
|
|
||||||
let edgeWidthChangeCleanup = [] as Array<() => void>;
|
let edgeWidthChangeCleanup = [] as Array<() => void>;
|
||||||
|
|
||||||
@@ -268,11 +240,36 @@ function onModelLoad() {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
if (!scene || !sceneModel) return;
|
||||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
|
||||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
// Count the number of faces, edges and vertices
|
||||||
// of not actually removing the primitives from the scene graph
|
const isFirstLoad = faceCount.value === -1;
|
||||||
|
faceCount.value = props.meshes
|
||||||
|
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
||||||
|
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
edgeCount.value = props.meshes
|
||||||
|
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
|
||||||
|
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
vertexCount.value = props.meshes
|
||||||
|
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
|
||||||
|
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
|
||||||
|
// First time: set the enabled features to all provided features
|
||||||
|
if (isFirstLoad) {
|
||||||
|
if (faceCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||||
|
else if (!enabledFeatures.value.includes(0)) enabledFeatures.value.push(0)
|
||||||
|
if (edgeCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||||
|
else if (!enabledFeatures.value.includes(1)) enabledFeatures.value.push(1)
|
||||||
|
if (vertexCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||||
|
else if (!enabledFeatures.value.includes(2)) enabledFeatures.value.push(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add darkened back faces for all face objects to improve cutting planes
|
||||||
let childrenToAdd: Array<MObject3D> = [];
|
let childrenToAdd: Array<MObject3D> = [];
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
|
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
||||||
// Compute a BVH for faster raycasting (MUCH faster selection)
|
// Compute a BVH for faster raycasting (MUCH faster selection)
|
||||||
@@ -319,95 +316,101 @@ function onModelLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// props.viewer.elem may not yet be available, so we need to wait for it
|
// props.viewer.elem may not yet be available, so we need to wait for it
|
||||||
props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
const onViewerReady = (viewer: InstanceType<typeof ModelViewerWrapper>) => {
|
||||||
|
viewer?.onElemReady((elem: HTMLElement) => {
|
||||||
|
elem.addEventListener('before-render', onModelLoad);
|
||||||
|
elem.addEventListener('camera-change', onClipPlanesChange);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) as any, onViewerReady);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-expansion-panel :value="modelName">
|
<v-expansion-panel :value="modelName">
|
||||||
<v-expansion-panel-title expand-icon="hide-this-icon" collapse-icon="hide-this-icon">
|
<v-expansion-panel-title collapse-icon="hide-this-icon" expand-icon="hide-this-icon">
|
||||||
<v-btn-toggle v-model="enabledFeatures" multiple @click.stop color="surface-light">
|
<v-btn-toggle v-model="enabledFeatures" color="surface-light" multiple @click.stop>
|
||||||
<v-btn icon>
|
<v-btn icon>
|
||||||
<v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip>
|
<v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiRectangle" :rotate="90"></svg-icon>
|
<svg-icon :path="mdiRectangle" :rotate="90" type="mdi"></svg-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon>
|
<v-btn icon>
|
||||||
<v-tooltip activator="parent">Toggle Edges ({{ edgeCount }})</v-tooltip>
|
<v-tooltip activator="parent">Toggle Edges ({{ edgeCount }})</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiRectangleOutline" :rotate="90"></svg-icon>
|
<svg-icon :path="mdiRectangleOutline" :rotate="90" type="mdi"></svg-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon>
|
<v-btn icon>
|
||||||
<v-tooltip activator="parent">Toggle Vertices ({{ vertexCount }})</v-tooltip>
|
<v-tooltip activator="parent">Toggle Vertices ({{ vertexCount }})</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiVectorRectangle" :rotate="90"></svg-icon>
|
<svg-icon :path="mdiVectorRectangle" :rotate="90" type="mdi"></svg-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-btn-toggle>
|
</v-btn-toggle>
|
||||||
<div class="model-name">{{ modelName }}</div>
|
<div class="model-name">{{ modelName }}</div>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn icon @click.stop="emit('remove')">
|
<v-btn icon @click.stop="emit('remove')">
|
||||||
<v-tooltip activator="parent">Remove</v-tooltip>
|
<v-tooltip activator="parent">Remove</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiDelete"></svg-icon>
|
<svg-icon :path="mdiDelete" type="mdi"></svg-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-expansion-panel-title>
|
</v-expansion-panel-title>
|
||||||
<v-expansion-panel-text>
|
<v-expansion-panel-text>
|
||||||
<v-slider v-model="opacity" hide-details min="0" max="1" :step="0.1">
|
<v-slider v-model="opacity" :step="0.1" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Change opacity</v-tooltip>
|
<v-tooltip activator="parent">Change opacity</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCircleOpacity"></svg-icon>
|
<svg-icon :path="mdiCircleOpacity" type="mdi"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Wireframe</v-tooltip>
|
<v-tooltip activator="parent">Wireframe</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-triangle-outline" falseIcon="mdi-triangle" v-model="wireframe"></v-checkbox-btn>
|
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details min="0" max="1">
|
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiVectorLine"></svg-icon>
|
<svg-icon :path="mdiVectorLine" type="mdi"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-slider v-model="clipPlaneX" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneX" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Clip plane X</v-tooltip>
|
<v-tooltip activator="parent">Clip plane X</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCube" :rotate="120"></svg-icon>
|
<svg-icon :path="mdiCube" :rotate="120" type="mdi"></svg-icon>
|
||||||
X
|
X
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Swap clip plane X</v-tooltip>
|
<v-tooltip activator="parent">Swap clip plane X</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
<v-checkbox-btn v-model="clipPlaneSwappedX" falseIcon="mdi-checkbox-blank-outline"
|
||||||
v-model="clipPlaneSwappedX">
|
trueIcon="mdi-checkbox-marked-outline">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneZ" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Clip plane Y</v-tooltip>
|
<v-tooltip activator="parent">Clip plane Y</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCube" :rotate="-120"></svg-icon>
|
<svg-icon :path="mdiCube" :rotate="-120" type="mdi"></svg-icon>
|
||||||
Y
|
Y
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
|
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
<v-checkbox-btn v-model="clipPlaneSwappedZ" falseIcon="mdi-checkbox-blank-outline"
|
||||||
v-model="clipPlaneSwappedZ">
|
trueIcon="mdi-checkbox-marked-outline">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-slider v-model="clipPlaneY" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneY" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Clip plane Z</v-tooltip>
|
<v-tooltip activator="parent">Clip plane Z</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCube"></svg-icon>
|
<svg-icon :path="mdiCube" type="mdi"></svg-icon>
|
||||||
Z
|
Z
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
|
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
<v-checkbox-btn v-model="clipPlaneSwappedY" falseIcon="mdi-checkbox-blank-outline"
|
||||||
v-model="clipPlaneSwappedY">
|
trueIcon="mdi-checkbox-marked-outline">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -441,11 +444,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.model-name {
|
.model-name {
|
||||||
width: 130px;
|
width: 179px;
|
||||||
min-height: 1.15em; /* HACK: Avoid eating the bottom of the text when using 1 line */
|
font-size: 110%;
|
||||||
max-height: 2em;
|
overflow-x: clip;
|
||||||
|
overflow-y: visible; /* HACK: bottom of text is lost otherwise (due to buggy -webkit-box bounds?) */
|
||||||
|
word-wrap: break-word;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2; /* https://caniuse.com/?search=line-clamp */
|
-webkit-line-clamp: 2; /* https://caniuse.com/?search=line-clamp */
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {VExpansionPanels} from "vuetify/lib/components/index.mjs";
|
import {VExpansionPanels} from "vuetify/lib/components/index.mjs";
|
||||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {Document, Mesh} from "@gltf-transform/core";
|
import {Document, Mesh} from "@gltf-transform/core";
|
||||||
@@ -36,9 +36,7 @@ function onRemove(mesh: Mesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findModel(name: string) {
|
function findModel(name: string) {
|
||||||
console.log('Find model', name);
|
|
||||||
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
||||||
console.log('Expanded', expandedNames.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({findModel})
|
defineExpose({findModel})
|
||||||
|
|||||||
2
frontend/shims.d.ts
vendored
2
frontend/shims.d.ts
vendored
@@ -1,3 +1,5 @@
|
|||||||
// Avoids typescript error when importing some files
|
// Avoids typescript error when importing some files
|
||||||
declare module '@jamescoyle/vue-icon'
|
declare module '@jamescoyle/vue-icon'
|
||||||
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
||||||
|
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
|
||||||
|
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
// License text for all dependencies, only downloaded when/if needed
|
// License text for all dependencies, only downloaded when/if needed
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import licenseText from "../../assets/licenses.txt?raw";
|
import licenseText from "../../assets/licenses.txt?raw";
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {onMounted, onUpdated, ref} from "vue";
|
import {onMounted, onUpdated, ref} from "vue";
|
||||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
|
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
|
||||||
import type {ModelViewerElement} from '@google/model-viewer';
|
|
||||||
|
|
||||||
// Optimized minimal dependencies from three
|
// Optimized minimal dependencies from three
|
||||||
import {Vector3} from "three/src/math/Vector3.js";
|
import {Vector3} from "three/src/math/Vector3.js";
|
||||||
import {Matrix4} from "three/src/math/Matrix4.js";
|
import {Matrix4} from "three/src/math/Matrix4.js";
|
||||||
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
|
|
||||||
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
||||||
|
|
||||||
const OrientationGizmo = OrientationGizmoRaw.default;
|
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> }>();
|
||||||
|
|
||||||
const props = defineProps<{ elem: ModelViewerElement | null, scene: ModelScene }>();
|
|
||||||
|
|
||||||
function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement {
|
function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement {
|
||||||
// noinspection SpellCheckingInspection
|
// noinspection SpellCheckingInspection
|
||||||
@@ -33,21 +31,26 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen
|
|||||||
}
|
}
|
||||||
// Append and listen for events
|
// Append and listen for events
|
||||||
gizmo.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => {
|
gizmo.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => {
|
||||||
let lookFrom = scene.getCamera().position.clone();
|
if (!props.viewer.elem || !props.viewer.controls) return;
|
||||||
let lookAt = scene.getTarget().clone().add(scene.target.position);
|
// Animate the controls to the new wanted angle
|
||||||
let magnitude = lookFrom.clone().sub(lookAt).length()
|
const controls = props.viewer.controls;
|
||||||
let direction = new Vector3(axis.direction.x, axis.direction.y, axis.direction.z);
|
const {theta: curTheta/*, phi: curPhi*/} = (controls as any).goalSpherical;
|
||||||
let newLookFrom = lookAt.clone().add(direction.clone().multiplyScalar(magnitude));
|
let wantedTheta = NaN;
|
||||||
//console.log("New camera position", newLookFrom)
|
let wantedPhi = NaN;
|
||||||
scene.getCamera().position.copy(newLookFrom);
|
let attempt = 0
|
||||||
scene.getCamera().lookAt(lookAt);
|
while ((attempt == 0 || curTheta == wantedTheta) && attempt < 2) {
|
||||||
if ((scene as any).__perspectiveCamera) { // HACK: Make the hacky ortho also work
|
if (attempt > 0) { // Flip the camera if the user clicks on the same axis
|
||||||
(scene as any).__perspectiveCamera.position.copy(newLookFrom);
|
axis.direction.x = -axis.direction.x;
|
||||||
(scene as any).__perspectiveCamera.lookAt(lookAt);
|
axis.direction.y = -axis.direction.y;
|
||||||
|
axis.direction.z = -axis.direction.z;
|
||||||
|
}
|
||||||
|
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z);
|
||||||
|
wantedPhi = Math.asin(-axis.direction.y) + Math.PI / 2;
|
||||||
|
attempt++;
|
||||||
}
|
}
|
||||||
|
controls.setOrbit(wantedTheta, wantedPhi);
|
||||||
|
props.viewer.elem?.dispatchEvent(new CustomEvent('camera-change', {detail: {source: 'none'}}))
|
||||||
scene.queueRender();
|
scene.queueRender();
|
||||||
requestIdleCallback(() => props.elem?.dispatchEvent(
|
|
||||||
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100})
|
|
||||||
}
|
}
|
||||||
return gizmo;
|
return gizmo;
|
||||||
}
|
}
|
||||||
@@ -65,9 +68,9 @@ function updateGizmo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reinstall = () => {
|
let reinstall = () => {
|
||||||
if(!container.value) return;
|
if (!container.value) return;
|
||||||
if (gizmo) container.value.removeChild(gizmo);
|
if (gizmo) container.value.removeChild(gizmo);
|
||||||
gizmo = createGizmo(container.value, props.scene as ModelScene) as typeof gizmo;
|
gizmo = createGizmo(container.value, props.viewer.scene!! as any) as typeof gizmo;
|
||||||
container.value.appendChild(gizmo);
|
container.value.appendChild(gizmo);
|
||||||
requestIdleCallback(updateGizmo, {timeout: 250}); // Low priority updates
|
requestIdleCallback(updateGizmo, {timeout: 250}); // Low priority updates
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
|
import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
|
||||||
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
|
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
|
||||||
import SvgIcon from '@jamescoyle/vue-icon';
|
import SvgIcon from '@jamescoyle/vue-icon';
|
||||||
@@ -33,21 +33,23 @@ let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
|||||||
let showDistances = ref<Boolean>(true);
|
let showDistances = ref<Boolean>(true);
|
||||||
|
|
||||||
let mouseDownAt: [number, number] | null = null;
|
let mouseDownAt: [number, number] | null = null;
|
||||||
|
let mouseDownTime = 0;
|
||||||
let selectFilter = ref('Any (S)');
|
let selectFilter = ref('Any (S)');
|
||||||
const raycaster = new Raycaster();
|
const raycaster = new Raycaster();
|
||||||
|
|
||||||
|
|
||||||
let selectionMoveListener = (event: MouseEvent) => {
|
let mouseDownListener = (event: MouseEvent) => {
|
||||||
mouseDownAt = [event.clientX, event.clientY];
|
mouseDownAt = [event.clientX, event.clientY];
|
||||||
|
mouseDownTime = performance.now();
|
||||||
if (!selectionEnabled.value) return;
|
if (!selectionEnabled.value) return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectionListener = (event: MouseEvent) => {
|
let mouseUpListener = (event: MouseEvent) => {
|
||||||
// If the mouse moved while clicked (dragging), avoid selection logic
|
// If the mouse moved while clicked (dragging), avoid selection logic
|
||||||
if (mouseDownAt) {
|
if (mouseDownAt) {
|
||||||
let [x, y] = mouseDownAt;
|
let [x, y] = mouseDownAt;
|
||||||
mouseDownAt = null;
|
mouseDownAt = null;
|
||||||
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) {
|
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5 || performance.now() - mouseDownTime > 500) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
|
|||||||
viewer.onElemReady((elem: ModelViewerElement) => {
|
viewer.onElemReady((elem: ModelViewerElement) => {
|
||||||
if (hasListeners) return;
|
if (hasListeners) return;
|
||||||
hasListeners = true;
|
hasListeners = true;
|
||||||
elem.addEventListener('mouseup', selectionListener);
|
elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
|
||||||
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
elem.addEventListener('mouseup', mouseUpListener);
|
||||||
elem.addEventListener('load', () => {
|
elem.addEventListener('before-render', () => {
|
||||||
|
// After a reload of the scene, we need to recover object references and highlight them again
|
||||||
|
for (let sel of selected.value) {
|
||||||
|
let scene = props.viewer?.scene;
|
||||||
|
if (!scene) continue;
|
||||||
|
let foundObject = null;
|
||||||
|
scene.traverse((obj: MObject3D) => {
|
||||||
|
if (sel.matches(obj)) {
|
||||||
|
foundObject = obj as MObject3D;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundObject) {
|
||||||
|
sel.object = foundObject;
|
||||||
|
highlight(sel);
|
||||||
|
} else {
|
||||||
|
selected.value = selected.value.filter((m) => m.getKey() !== sel.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
toggleShowBoundingBox();
|
toggleShowBoundingBox();
|
||||||
firstLoad = false;
|
firstLoad = false;
|
||||||
} else {
|
|
||||||
updateBoundingBox();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
elem.addEventListener('camera-change', onCameraChange);
|
elem.addEventListener('camera-change', onCameraChange);
|
||||||
@@ -406,6 +423,8 @@ function updateDistances() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({deselect, updateBoundingBox, updateDistances});
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 's') {
|
if (event.key === 's') {
|
||||||
@@ -443,52 +462,41 @@ window.addEventListener('keydown', (event) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="select-parent">
|
<v-btn :color="selectionEnabled ? 'surface-light' : ''" icon @click="toggleSelection">
|
||||||
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
|
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
|
||||||
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
|
|
||||||
</v-tooltip>
|
|
||||||
<svg-icon type="mdi" :path="mdiCursorDefaultClick"/>
|
|
||||||
</v-btn>
|
|
||||||
<v-tooltip :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()" :open-on-click="false">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-select v-bind="props" class="select-only" variant="underlined"
|
|
||||||
:items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']"
|
|
||||||
v-model="selectFilter"/>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</div>
|
<svg-icon :path="mdiCursorDefaultClick" type="mdi"/>
|
||||||
<v-btn icon @click="toggleHighlightNextSelection" :color="highlightNextSelection[0] ? 'surface-light' : ''">
|
|
||||||
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
|
|
||||||
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
|
<v-tooltip :open-on-click="false" :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-select v-model="selectFilter" :items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']" class="select-only"
|
||||||
|
v-bind="props"
|
||||||
|
variant="underlined"/>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-btn :color="highlightNextSelection[0] ? 'surface-light' : ''" icon @click="toggleHighlightNextSelection">
|
||||||
|
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
|
||||||
|
<svg-icon :path="mdiFeatureSearch" type="mdi"/>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn :color="showBoundingBox ? 'surface-light' : ''" icon @click="toggleShowBoundingBox">
|
||||||
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection (b)ounds' : 'Show selection (b)ounds' }}
|
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection (b)ounds' : 'Show selection (b)ounds' }}
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCubeOutline"/>
|
<svg-icon :path="mdiCubeOutline" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon @click="toggleShowDistances" :color="showDistances ? 'surface-light' : ''">
|
<v-btn :color="showDistances ? 'surface-light' : ''" icon @click="toggleShowDistances">
|
||||||
<v-tooltip activator="parent">
|
<v-tooltip activator="parent">
|
||||||
{{ showDistances ? 'Hide selection (d)istances' : 'Show (d)istances (when a pair of features is selected)' }}
|
{{ showDistances ? 'Hide selection (d)istances' : 'Show (d)istances (when a pair of features is selected)' }}
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiRuler"/>
|
<svg-icon :path="mdiRuler" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Very hacky styling... */
|
|
||||||
.select-parent {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-parent .v-btn {
|
|
||||||
position: relative;
|
|
||||||
top: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-only {
|
.select-only {
|
||||||
display: inline-block;
|
float: right;
|
||||||
width: calc(100% - 48px);
|
height: 36px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -12px;
|
top: -12px;
|
||||||
|
width: calc(100% - 48px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
VBtn,
|
VBtn,
|
||||||
VCard,
|
VCard,
|
||||||
@@ -16,10 +16,9 @@ import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
|
|||||||
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
||||||
import SvgIcon from '@jamescoyle/vue-icon';
|
import SvgIcon from '@jamescoyle/vue-icon';
|
||||||
import type {ModelViewerElement} from '@google/model-viewer';
|
import type {ModelViewerElement} from '@google/model-viewer';
|
||||||
import type {MObject3D} from "./Selection.vue";
|
|
||||||
import Loading from "../misc/Loading.vue";
|
import Loading from "../misc/Loading.vue";
|
||||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {defineAsyncComponent, type Ref, ref} from "vue";
|
import {defineAsyncComponent, ref, type Ref} from "vue";
|
||||||
import type {SelectionInfo} from "./selection";
|
import type {SelectionInfo} from "./selection";
|
||||||
|
|
||||||
const SelectionComponent = defineAsyncComponent({
|
const SelectionComponent = defineAsyncComponent({
|
||||||
@@ -57,14 +56,14 @@ function syncOrthoCamera(force: boolean) {
|
|||||||
let h = perspectiveWidthAtCenter / scene.aspect;
|
let h = perspectiveWidthAtCenter / scene.aspect;
|
||||||
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
|
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
|
||||||
scene.camera.position.copy(perspectiveCam.position);
|
scene.camera.position.copy(perspectiveCam.position);
|
||||||
scene.camera.lookAt(lookAtCenter);
|
scene.camera.rotation.copy(perspectiveCam.rotation);
|
||||||
if (force) scene.queueRender() // Force rerender of model-viewer
|
if (force) scene.queueRender() // Force rerender of model-viewer
|
||||||
requestAnimationFrame(() => syncOrthoCamera(false));
|
requestAnimationFrame(() => syncOrthoCamera(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let toggleProjectionText = ref('PERSP'); // Default to perspective camera
|
let toggleProjectionText = ref('PERSP'); // Default to perspective camera
|
||||||
function toggleProjection() {
|
async function toggleProjection() {
|
||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
if (!scene) return;
|
if (!scene) return;
|
||||||
let prevCam = scene.camera;
|
let prevCam = scene.camera;
|
||||||
@@ -79,16 +78,16 @@ function toggleProjection() {
|
|||||||
scene.queueRender() // Force rerender of model-viewer
|
scene.queueRender() // Force rerender of model-viewer
|
||||||
}
|
}
|
||||||
toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP';
|
toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP';
|
||||||
// The camera change may take a few frames to take effect, dispatch the event after a delay
|
// The camera change may take a frame to take effect, dispatch the event after a delay
|
||||||
requestIdleCallback(() => props.viewer?.elem?.dispatchEvent(
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100})
|
props.viewer?.elem?.dispatchEvent(new CustomEvent('camera-change', {detail: {source: 'none'}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function centerCamera() {
|
async function centerCamera() {
|
||||||
let viewerEl: ModelViewerElement | null | undefined = props.viewer?.elem;
|
let viewerEl: ModelViewerElement | null | undefined = props.viewer?.elem;
|
||||||
if (!viewerEl) return;
|
if (!viewerEl) return;
|
||||||
await viewerEl.updateFraming();
|
props.viewer?.scene?.setTarget(0, 0, 0); // Center the target
|
||||||
viewerEl.zoom(3);
|
viewerEl.zoom(-1000000); // Max zoom out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +106,15 @@ async function openGithub() {
|
|||||||
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank')
|
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeObjectSelections(objName: string) {
|
||||||
|
for (let selInfo of selection.value.filter((s) => s.getObjectName() === objName)) {
|
||||||
|
selectionComp.value?.deselect(selInfo);
|
||||||
|
}
|
||||||
|
selectionComp.value?.updateBoundingBox();
|
||||||
|
selectionComp.value?.updateDistances();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({removeObjectSelections});
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
@@ -118,35 +126,35 @@ window.addEventListener('keydown', (event) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<orientation-gizmo :scene="props.viewer.scene as any" :elem="props.viewer.elem" v-if="props.viewer?.scene"/>
|
<orientation-gizmo v-if="props.viewer?.scene" :viewer="props.viewer"/>
|
||||||
<v-divider/>
|
<v-divider/>
|
||||||
<h5>Camera</h5>
|
<h5>Camera</h5>
|
||||||
<v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span>
|
<v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span>
|
||||||
<v-tooltip activator="parent">Toggle (P)rojection<br/>(currently
|
<v-tooltip activator="parent">Toggle (P)rojection<br/>(currently
|
||||||
{{ toggleProjectionText === 'PERSP' ? 'perspective' : 'orthographic' }})
|
{{ toggleProjectionText === 'PERSP' ? 'perspective' : 'orthographic' }})
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiProjector"></svg-icon>
|
<svg-icon :path="mdiProjector" type="mdi"></svg-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon @click="centerCamera">
|
<v-btn icon @click="centerCamera">
|
||||||
<v-tooltip activator="parent">Re(c)enter Camera</v-tooltip>
|
<v-tooltip activator="parent">Re(c)enter Camera</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCrosshairsGps"/>
|
<svg-icon :path="mdiCrosshairsGps" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-divider/>
|
<v-divider/>
|
||||||
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
|
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
|
||||||
<selection-component :ref="selectionComp as any" :viewer="props.viewer as any" v-model="selection"
|
<selection-component ref="selectionComp" v-model="selection" :viewer="props.viewer as any"
|
||||||
@findModel="(name) => emit('findModel', name)"/>
|
@findModel="(name) => emit('findModel', name)"/>
|
||||||
<v-divider/>
|
<v-divider/>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<h5>Extras</h5>
|
<h5>Extras</h5>
|
||||||
<v-btn icon @click="downloadSceneGlb">
|
<v-btn icon @click="downloadSceneGlb">
|
||||||
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
|
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiDownload"/>
|
<svg-icon :path="mdiDownload" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-dialog id="licenses-dialog" fullscreen>
|
<v-dialog id="licenses-dialog" fullscreen>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn icon v-bind="props">
|
<v-btn icon v-bind="props">
|
||||||
<v-tooltip activator="parent">Show Licenses</v-tooltip>
|
<v-tooltip activator="parent">Show Licenses</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiLicense"/>
|
<svg-icon :path="mdiLicense" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:default="{ isActive }">
|
<template v-slot:default="{ isActive }">
|
||||||
@@ -156,7 +164,7 @@ window.addEventListener('keydown', (event) => {
|
|||||||
<v-spacer>
|
<v-spacer>
|
||||||
</v-spacer>
|
</v-spacer>
|
||||||
<v-btn icon @click="isActive.value = false">
|
<v-btn icon @click="isActive.value = false">
|
||||||
<svg-icon type="mdi" :path="mdiClose"/>
|
<svg-icon :path="mdiClose" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
@@ -167,7 +175,7 @@ window.addEventListener('keydown', (event) => {
|
|||||||
</v-dialog>
|
</v-dialog>
|
||||||
<v-btn icon @click="openGithub">
|
<v-btn icon @click="openGithub">
|
||||||
<v-tooltip activator="parent">Open (G)itHub</v-tooltip>
|
<v-tooltip activator="parent">Open (G)itHub</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiGithub"/>
|
<svg-icon :path="mdiGithub" type="mdi"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<div ref="statsHolder"></div>
|
<div ref="statsHolder"></div>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,4 +195,8 @@ window.addEventListener('keydown', (event) => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import type {MObject3D} from "./Selection.vue";
|
import type {MObject3D} from "./Selection.vue";
|
||||||
import type {Intersection} from "three";
|
import type {Intersection} from "three";
|
||||||
import {Box3} from "three";
|
import {Box3} from "three";
|
||||||
|
import {extrasNameKey} from "../misc/gltf";
|
||||||
|
|
||||||
/** Information about a single item in the selection */
|
/** Information about a single item in the selection */
|
||||||
export class SelectionInfo {
|
export class SelectionInfo {
|
||||||
@@ -19,6 +20,17 @@ export class SelectionInfo {
|
|||||||
this.indices = indices;
|
this.indices = indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getObjectName() {
|
||||||
|
return this.object.userData[extrasNameKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
public matches(object: MObject3D) {
|
||||||
|
return this.getObjectName() === object.userData[extrasNameKey] &&
|
||||||
|
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
|
||||||
|
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
|
||||||
|
this.kind === 'vertex' && object.type === 'Points')
|
||||||
|
}
|
||||||
|
|
||||||
public getKey() {
|
public getKey() {
|
||||||
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {settings} from "../misc/settings";
|
import {settings} from "../misc/settings";
|
||||||
import {inject, onMounted, type Ref, ref, watch} from "vue";
|
import {inject, onMounted, type Ref, ref, watch} from "vue";
|
||||||
import {VList, VListItem} from "vuetify/lib/components/index.mjs";
|
|
||||||
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
|
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';
|
||||||
import {ModelViewerElement} from '@google/model-viewer';
|
import {ModelViewerElement} from '@google/model-viewer';
|
||||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
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
|
||||||
@@ -19,30 +20,96 @@ BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
Mesh.prototype.raycast = acceleratedRaycast;
|
Mesh.prototype.raycast = acceleratedRaycast;
|
||||||
|
|
||||||
const emit = defineEmits<{ load: [] }>()
|
|
||||||
|
|
||||||
const props = defineProps<{ src: string }>();
|
const props = defineProps<{ src: string }>();
|
||||||
|
|
||||||
const elem = ref<ModelViewerElement | null>(null);
|
const elem = ref<ModelViewerElement | null>(null);
|
||||||
const scene = ref<ModelScene | null>(null);
|
const scene = ref<ModelScene | null>(null);
|
||||||
const renderer = ref<Renderer | null>(null);
|
const renderer = ref<Renderer | null>(null);
|
||||||
|
const controls = ref<SmoothControls | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
let lastCameraTargetPosition: Vector3 | undefined = undefined;
|
||||||
|
let lastCameraZoom: number | undefined = undefined;
|
||||||
|
let lastCameraUrl = props.src.toString();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!elem.value) return;
|
if (!elem.value) return;
|
||||||
elem.value.addEventListener('load', async () => {
|
elem.value.addEventListener('before-render', () => {
|
||||||
if (!elem.value) return;
|
if (!elem.value) return;
|
||||||
// Delete the initial load banner
|
// Extract internals of model-viewer in order to hack unsupported features
|
||||||
let banner = elem.value.querySelector('.initial-load-banner');
|
|
||||||
if (banner) banner.remove();
|
|
||||||
// Set the scene and renderer
|
|
||||||
scene.value = elem.value[$scene] as ModelScene;
|
scene.value = elem.value[$scene] as ModelScene;
|
||||||
renderer.value = elem.value[$renderer] as Renderer;
|
renderer.value = elem.value[$renderer] as Renderer;
|
||||||
// Emit the load event
|
controls.value = (elem.value as any)[$controls] as SmoothControls;
|
||||||
emit('load')
|
// Recover the camera position if it was set before
|
||||||
|
if (lastCameraTargetPosition) {
|
||||||
|
// console.log("RESTORING camera position?", lastCameraTargetPosition);
|
||||||
|
scene.value.setTarget(-lastCameraTargetPosition.x, -lastCameraTargetPosition.y, -lastCameraTargetPosition.z);
|
||||||
|
scene.value.jumpToGoal(); // Avoid move animation
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
let tries = 0
|
||||||
|
while (tries++ < 25) {
|
||||||
|
if (!lastCameraZoom || !elem.value?.getCameraOrbit()?.radius) break;
|
||||||
|
let change = lastCameraZoom - elem.value.getCameraOrbit().radius;
|
||||||
|
//console.log("Zooming to", lastCameraZoom, "from", elem.value.getCameraOrbit().radius, "change", change);
|
||||||
|
if (Math.abs(change) < 0.001) break;
|
||||||
|
elem.value.zoom(-Math.sign(change) * (Math.pow(Math.abs(change) + 1, 0.9) - 1)); // Arbitrary, experimental
|
||||||
|
elem.value.jumpCameraToGoal();
|
||||||
|
await elem.value.updateComplete;
|
||||||
|
}
|
||||||
|
//console.log("Ready to save!")
|
||||||
|
lastCameraUrl = props.src.toString();
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
elem.value.addEventListener('camera-change', onCameraChange);
|
elem.value.addEventListener('camera-change', onCameraChange);
|
||||||
|
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onCameraChange() {
|
||||||
|
// Remember the camera position to keep it in case of scene changes
|
||||||
|
if (scene.value && props.src.toString() == lastCameraUrl) { // Don't overwrite with initial unwanted positions
|
||||||
|
lastCameraTargetPosition = scene.value.target.position.clone();
|
||||||
|
lastCameraZoom = elem.value?.getCameraOrbit()?.radius;
|
||||||
|
//console.log("Saving camera?", lastCameraTargetPosition, lastCameraZoom);
|
||||||
|
}
|
||||||
|
// Also need to update the SVG overlay
|
||||||
|
for (let lineId in lines.value) {
|
||||||
|
onCameraChangeLine(lineId as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const poster = ref<string>("")
|
||||||
|
const setPosterText = (newText: string) => {
|
||||||
|
poster.value = "data:image/svg+xml;charset=utf-8;base64," + btoa(
|
||||||
|
'<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg" fill="gray">' +
|
||||||
|
'<text x="50%" y="0%" dominant-baseline="middle" text-anchor="middle" font-size="48px">' +
|
||||||
|
newText +
|
||||||
|
'</text>' +
|
||||||
|
'</svg>')
|
||||||
|
}
|
||||||
|
setPosterText("Loading...")
|
||||||
|
|
||||||
class Line3DData {
|
class Line3DData {
|
||||||
startHotspot: HTMLElement = document.body
|
startHotspot: HTMLElement = document.body
|
||||||
endHotspot: HTMLElement = document.body
|
endHotspot: HTMLElement = document.body
|
||||||
@@ -95,13 +162,6 @@ function removeLine3D(id: number): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCameraChange() {
|
|
||||||
// Need to update the SVG overlay
|
|
||||||
for (let lineId in lines.value) {
|
|
||||||
onCameraChangeLine(lineId as any);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let svg = ref<SVGElement | null>(null);
|
let svg = ref<SVGElement | null>(null);
|
||||||
|
|
||||||
function onCameraChangeLine(lineId: number) {
|
function onCameraChangeLine(lineId: number) {
|
||||||
@@ -137,48 +197,47 @@ 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, controls, addLine3D, removeLine3D, onProgress, setPosterText});
|
||||||
|
|
||||||
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
||||||
watch(disableTap, (value) => {
|
watch(disableTap, (newDisableTap) => {
|
||||||
// Rerender not auto triggered? This works anyway...
|
if (elem.value) elem.value.disableTap = newDisableTap;
|
||||||
if (value) elem.value?.setAttribute('disable-tap', '');
|
|
||||||
else elem.value?.removeAttribute('disable-tap');
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- The main 3D model viewer -->
|
<!-- The main 3D model viewer -->
|
||||||
<model-viewer ref="elem" style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls
|
<model-viewer ref="elem" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :autoplay="settings.autoplay"
|
||||||
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto"
|
:environment-image="settings.background" :exposure="settings.exposure"
|
||||||
min-camera-orbit="-Infinity 0deg 5%" :disable-tap="disableTap" :exposure="settings.exposure"
|
:orbit-sensitivity="settings.orbitSensitivity" :pan-sensitivity="settings.panSensitivity"
|
||||||
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
|
:poster="poster" :shadow-intensity="settings.shadowIntensity" :skybox-image="settings.background"
|
||||||
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
|
:src="props.src" :zoom-sensitivity="settings.zoomSensitivity" alt="The 3D model(s)" camera-controls
|
||||||
:environment-image="settings.background">
|
camera-orbit="30deg 75deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
|
||||||
<slot></slot> <!-- Controls, annotations, etc. -->
|
min-camera-orbit="-Infinity 0deg 5%" style="width: 100%; height: 100%">
|
||||||
<div class="annotation initial-load-banner">
|
<slot></slot>
|
||||||
Trying to load models from...
|
<!-- Add a progress bar to the top of the model viewer -->
|
||||||
<v-list v-for="src in settings.preload" :key="src">
|
<div ref="progressBar" slot="progress-bar" class="progress-bar">
|
||||||
<v-list-item>{{ src }}</v-list-item>
|
<div ref="updateBar" class="update-bar"/>
|
||||||
</v-list>
|
|
||||||
<!-- Too much idle CPU usage: <loading></loading> -->
|
|
||||||
</div>
|
</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 -->
|
||||||
<div class="overlay-svg-wrapper">
|
<div class="overlay-svg-wrapper">
|
||||||
<svg ref="svg" class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
<svg ref="svg" class="overlay-svg" height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g v-for="[lineId, line] in entries(lines)" :key="lineId">
|
<g v-for="[lineId, line] in entries(lines)" :key="lineId">
|
||||||
<line :x1="line.start2D[0]" :y1="line.start2D[1]" :x2="line.end2D[0]"
|
<line :x1="line.start2D[0]" :x2="line.end2D[0]" :y1="line.start2D[1]"
|
||||||
:y2="line.end2D[1]" v-bind="line.lineAttrs"/>
|
:y2="line.end2D[1]" v-bind="line.lineAttrs"/>
|
||||||
<g v-if="line.centerText !== undefined">
|
<g v-if="line.centerText !== undefined">
|
||||||
<rect :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"
|
<rect v-if="line.centerText"
|
||||||
:y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2"
|
:height="line.centerTextSize[1] + 4"
|
||||||
:width="line.centerTextSize[0] + 8" :height="line.centerTextSize[1] + 4"
|
:width="line.centerTextSize[0] + 8"
|
||||||
fill="gray" fill-opacity="0.75" rx="4" ry="4" stroke="black" v-if="line.centerText"/>
|
:x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"
|
||||||
<text :x="(line.start2D[0] + line.end2D[0]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2"
|
:y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2" fill="gray"
|
||||||
text-anchor="middle" dominant-baseline="middle" font-size="16" fill="black"
|
fill-opacity="0.75" rx="4" ry="4" stroke="black"/>
|
||||||
:class="'line' + lineId + '_text'" v-if="line.centerText">
|
<text v-if="line.centerText" :class="'line' + lineId + '_text'"
|
||||||
|
:x="(line.start2D[0] + line.end2D[0]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2"
|
||||||
|
dominant-baseline="middle" fill="black"
|
||||||
|
font-size="16" text-anchor="middle">
|
||||||
{{ line.centerText }}
|
{{ line.centerText }}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
@@ -213,14 +272,29 @@ watch(disableTap, (value) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-load-banner {
|
.progress-bar {
|
||||||
width: 300px;
|
display: block;
|
||||||
margin: auto;
|
pointer-events: none;
|
||||||
margin-top: 3em;
|
width: 100%;
|
||||||
overflow: hidden;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-load-banner .v-list-item {
|
.update-bar {
|
||||||
overflow: hidden;
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 25px;
|
||||||
|
float: left;
|
||||||
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yet-another-cad-viewer",
|
"name": "yet-another-cad-viewer",
|
||||||
"version": "0.8.2",
|
"version": "0.8.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gltf-transform/core": "^3.10.0",
|
"@gltf-transform/core": "^3.10.1",
|
||||||
|
"@gltf-transform/extensions": "^3.10.1",
|
||||||
"@gltf-transform/functions": "^3.10.1",
|
"@gltf-transform/functions": "^3.10.1",
|
||||||
"@google/model-viewer": "^3.4.0",
|
"@google/model-viewer": "^3.4.0",
|
||||||
"@jamescoyle/vue-icon": "^0.1.2",
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
@@ -25,11 +26,11 @@
|
|||||||
"three-mesh-bvh": "^0.7.3",
|
"three-mesh-bvh": "^0.7.3",
|
||||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vuetify": "^3.5.11"
|
"vuetify": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.3",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.12.2",
|
||||||
"@types/three": "^0.160.0",
|
"@types/three": "^0.160.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
@@ -38,9 +39,9 @@
|
|||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"generate-license-file": "^3.0.1",
|
"generate-license-file": "^3.0.1",
|
||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
"terser": "^5.29.2",
|
"terser": "^5.30.0",
|
||||||
"typescript": "~5.4.3",
|
"typescript": "~5.4.3",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.7",
|
||||||
"vue-tsc": "^2.0.7"
|
"vue-tsc": "^2.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "yacv-server"
|
name = "yacv-server"
|
||||||
version = "0.8.2"
|
version = "0.8.9"
|
||||||
description = "Yet Another CAD Viewer (server)"
|
description = "Yet Another CAD Viewer (server)"
|
||||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
assetsDir: '.',
|
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
|
||||||
cssCodeSplit: false, // Small enough to inline
|
cssCodeSplit: false, // Small enough to inline
|
||||||
chunkSizeWarningLimit: 550, // Three.js is huge
|
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
|
||||||
|
sourcemap: true, // For debugging production
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__APP_NAME__: JSON.stringify(name),
|
__APP_NAME__: JSON.stringify(name),
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
Utilities to work with CAD objects
|
Utilities to work with CAD objects
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Optional, Union, List, Tuple
|
import io
|
||||||
|
import re
|
||||||
|
from typing import Optional, Union, Tuple
|
||||||
|
|
||||||
|
from OCP.TopExp import TopExp
|
||||||
from OCP.TopLoc import TopLoc_Location
|
from OCP.TopLoc import TopLoc_Location
|
||||||
|
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||||
from OCP.TopoDS import TopoDS_Shape
|
from OCP.TopoDS import TopoDS_Shape
|
||||||
|
from build123d import Compound, Shape
|
||||||
|
|
||||||
from yacv_server.gltf import GLTFMgr
|
from yacv_server.gltf import GLTFMgr
|
||||||
|
|
||||||
@@ -40,22 +45,42 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
|||||||
if isinstance(obj, TopoDS_Shape):
|
if isinstance(obj, TopoDS_Shape):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
|
||||||
|
if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, set) or isinstance(obj, dict):
|
||||||
|
try:
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
obj_iter = iter(obj.values())
|
||||||
|
else:
|
||||||
|
obj_iter = iter(obj)
|
||||||
|
# print(obj, ' -> ', obj_iter)
|
||||||
|
shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
|
||||||
|
# Silently drop non-shapes
|
||||||
|
shapes_raw_filtered = [shape for shape in shapes_raw if shape is not None]
|
||||||
|
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
|
||||||
|
# Sorting is required to improve hashcode consistency
|
||||||
|
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
|
||||||
|
# Build a single compound shape
|
||||||
|
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
|
||||||
|
return get_shape(Compound(shapes_bd), error)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
||||||
else:
|
else:
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -137,3 +162,32 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
|||||||
|
|
||||||
# Return the GLTF binary blob and the suggested name of the image
|
# Return the GLTF binary blob and the suggested name of the image
|
||||||
return b''.join(mgr.build().save_to_bytes()), name
|
return b''.join(mgr.build().save_to_bytes()), name
|
||||||
|
|
||||||
|
|
||||||
|
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
|
||||||
|
"""Utility to compute the STABLE hash code of a shape"""
|
||||||
|
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
||||||
|
# This is best-effort and not guaranteed to be unique
|
||||||
|
hasher = hashlib.md5(usedforsecurity=False)
|
||||||
|
for k, v in extras.items():
|
||||||
|
hasher.update(str(k).encode())
|
||||||
|
hasher.update(str(v).encode())
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
hasher.update(obj)
|
||||||
|
elif isinstance(obj, TopLoc_Location):
|
||||||
|
sub_data = io.BytesIO()
|
||||||
|
obj.DumpJson(sub_data)
|
||||||
|
hasher.update(sub_data.getvalue())
|
||||||
|
elif isinstance(obj, TopoDS_Shape):
|
||||||
|
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||||
|
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||||
|
for i in range(1, map_of_shapes.Extent() + 1):
|
||||||
|
sub_shape = map_of_shapes.FindKey(i)
|
||||||
|
sub_data = io.BytesIO()
|
||||||
|
TopoDS_Shape.DumpJson(sub_shape, sub_data)
|
||||||
|
val = sub_data.getvalue()
|
||||||
|
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
||||||
|
hasher.update(val)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import hashlib
|
from typing import List, Dict, Tuple
|
||||||
import io
|
|
||||||
import re
|
|
||||||
from typing import List, Dict, Tuple, Union
|
|
||||||
|
|
||||||
from OCP.BRep import BRep_Tool
|
from OCP.BRep import BRep_Tool
|
||||||
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
||||||
from OCP.GCPnts import GCPnts_TangentialDeflection
|
from OCP.GCPnts import GCPnts_TangentialDeflection
|
||||||
from OCP.TopExp import TopExp
|
|
||||||
from OCP.TopLoc import TopLoc_Location
|
from OCP.TopLoc import TopLoc_Location
|
||||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
|
||||||
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
||||||
from build123d import Shape, Vertex, Face, Location
|
from build123d import Shape, Vertex, Face, Location
|
||||||
from pygltflib import GLTF2
|
from pygltflib import GLTF2
|
||||||
@@ -130,30 +125,3 @@ def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[Topo
|
|||||||
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
|
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
|
||||||
|
|
||||||
|
|
||||||
def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
|
|
||||||
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
|
|
||||||
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
|
||||||
# This is best-effort and not guaranteed to be unique
|
|
||||||
hasher = hashlib.md5(usedforsecurity=False)
|
|
||||||
for k, v in extras.items():
|
|
||||||
hasher.update(str(k).encode())
|
|
||||||
hasher.update(str(v).encode())
|
|
||||||
if isinstance(obj, bytes):
|
|
||||||
hasher.update(obj)
|
|
||||||
elif isinstance(obj, TopLoc_Location):
|
|
||||||
sub_data = io.BytesIO()
|
|
||||||
obj.DumpJson(sub_data)
|
|
||||||
hasher.update(sub_data.getvalue())
|
|
||||||
elif isinstance(obj, TopoDS_Shape):
|
|
||||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
|
||||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
|
||||||
for i in range(1, map_of_shapes.Extent() + 1):
|
|
||||||
sub_shape = map_of_shapes.FindKey(i)
|
|
||||||
sub_data = io.BytesIO()
|
|
||||||
TopoDS_Shape.DumpJson(sub_shape, sub_data)
|
|
||||||
val = sub_data.getvalue()
|
|
||||||
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
|
||||||
hasher.update(val)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
|
||||||
return hasher.hexdigest()
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from yacv_server.myhttp import HTTPHandler
|
|||||||
from yacv_server.mylogger import logger
|
from yacv_server.mylogger import logger
|
||||||
from yacv_server.pubsub import BufferedPubSub
|
from yacv_server.pubsub import BufferedPubSub
|
||||||
from yacv_server.rwlock import RWLock
|
from yacv_server.rwlock import RWLock
|
||||||
from yacv_server.tessellate import _hashcode, tessellate
|
from yacv_server.tessellate import tessellate
|
||||||
|
from yacv_server.cad import _hashcode
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
@dataclass_json
|
||||||
@@ -194,7 +195,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):
|
||||||
|
|||||||
46
yarn.lock
46
yarn.lock
@@ -390,7 +390,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||||
|
|
||||||
"@gltf-transform/core@^3.10.0", "@gltf-transform/core@^3.10.1":
|
"@gltf-transform/core@^3.10.1":
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
||||||
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
||||||
@@ -777,10 +777,10 @@
|
|||||||
"@sigstore/core" "^1.0.0"
|
"@sigstore/core" "^1.0.0"
|
||||||
"@sigstore/protobuf-specs" "^0.3.0"
|
"@sigstore/protobuf-specs" "^0.3.0"
|
||||||
|
|
||||||
"@tsconfig/node20@^20.1.3":
|
"@tsconfig/node20@^20.1.4":
|
||||||
version "20.1.3"
|
version "20.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.3.tgz#b3b4cf785e1b390a6ab48a68aa594a25960d2fe8"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||||
integrity sha512-XeWn6Gms5MaQWdj+C4fuxuo/Icy8ckh+BwAIijhX2LKRHHt1OuctLLLlB0F4EPi55m2IUJNTnv8FH9kSBI7Ogw==
|
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||||
|
|
||||||
"@tufjs/canonical-json@2.0.0":
|
"@tufjs/canonical-json@2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -805,10 +805,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
||||||
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
||||||
|
|
||||||
"@types/node@^20.11.30":
|
"@types/node@^20.12.2":
|
||||||
version "20.11.30"
|
version "20.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
||||||
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
|
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
@@ -2410,7 +2410,7 @@ postcss-selector-parser@^6.0.10:
|
|||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
util-deprecate "^1.0.2"
|
util-deprecate "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.4.35, postcss@^8.4.36:
|
postcss@^8.4.35, postcss@^8.4.38:
|
||||||
version "8.4.38"
|
version "8.4.38"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||||
@@ -2883,10 +2883,10 @@ tar@^6.1.11, tar@^6.1.2:
|
|||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
terser@^5.29.2:
|
terser@^5.30.0:
|
||||||
version "5.29.2"
|
version "5.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
|
||||||
integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==
|
integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/source-map" "^0.3.3"
|
"@jridgewell/source-map" "^0.3.3"
|
||||||
acorn "^8.8.2"
|
acorn "^8.8.2"
|
||||||
@@ -3002,13 +3002,13 @@ validate-npm-package-name@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
builtins "^5.0.0"
|
builtins "^5.0.0"
|
||||||
|
|
||||||
vite@^5.2.6:
|
vite@^5.2.7:
|
||||||
version "5.2.6"
|
version "5.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
|
||||||
integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==
|
integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.20.1"
|
esbuild "^0.20.1"
|
||||||
postcss "^8.4.36"
|
postcss "^8.4.38"
|
||||||
rollup "^4.13.0"
|
rollup "^4.13.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
@@ -3041,10 +3041,10 @@ vue@^3.4.21:
|
|||||||
"@vue/server-renderer" "3.4.21"
|
"@vue/server-renderer" "3.4.21"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.21"
|
||||||
|
|
||||||
vuetify@^3.5.11:
|
vuetify@^3.5.13:
|
||||||
version "3.5.11"
|
version "3.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.11.tgz#9e5b628544e736de0b7f236b704539d544588152"
|
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
|
||||||
integrity sha512-us5I0jyFwIQYG4v41PFmVMkoc/oJddVT4C2RFjJTI99ttigbQ92gsTeG5SB8BPfmfnUS4paR5BedZwk6W3KlJw==
|
integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
|
||||||
|
|
||||||
walk-up-path@^3.0.1:
|
walk-up-path@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user