better model updates, broken tool/selection init

This commit is contained in:
Yeicor
2024-03-02 19:24:54 +01:00
parent 2ff9ac9e7e
commit a7f07d172e
7 changed files with 77 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
<!--suppress SillyAssignmentJS --> <!--suppress SillyAssignmentJS -->
<script setup lang="ts"> <script setup lang="ts">
import {defineAsyncComponent, provide, Ref, ref, shallowRef} from "vue"; import {defineAsyncComponent, provide, Ref, ref, shallowRef, triggerRef} 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";
@@ -23,25 +23,23 @@ const ModelViewerWrapper = defineAsyncComponent({
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200); let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
let sceneUrl = ref("") const sceneUrl = ref("")
let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null); const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
let document = shallowRef(new Document()); const sceneDocument = shallowRef(new Document());
let models: Ref<InstanceType<typeof Models> | null> = ref(null) provide('sceneDocument', {sceneDocument});
provide('document', document); const models: Ref<InstanceType<typeof Models> | null> = ref(null)
let disableTap = ref(false); const disableTap = ref(false);
let setDisableTap = (val: boolean) => { const setDisableTap = (val: boolean) => disableTap.value = val;
disableTap.value = val;
}
provide('disableTap', {disableTap, setDisableTap}); provide('disableTap', {disableTap, setDisableTap});
async function onModelLoadRequest(model: NetworkUpdateEvent) { async function onModelLoadRequest(model: NetworkUpdateEvent) {
await SceneMgr.loadModel(sceneUrl, document, model.name, model.url); sceneDocument.value = await SceneMgr.loadModel(sceneUrl, sceneDocument.value, model.name, model.url);
document.value = document.value.clone(); // Force update from this component! triggerRef(sceneDocument); // Why not triggered automatically?
} }
function onModelRemoveRequest(name: string) { async function onModelRemoveRequest(name: string) {
SceneMgr.removeModel(sceneUrl, document, name); sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
document.value = document.value.clone(); // Force update from this component! triggerRef(sceneDocument); // Why not triggered automatically?
} }
// Set up the load model event listener // Set up the load model event listener
@@ -76,7 +74,7 @@ async function loadModelManual() {
<svg-icon type="mdi" :path="mdiPlus"/> <svg-icon type="mdi" :path="mdiPlus"/>
</v-btn> </v-btn>
</template> </template>
<models ref="models" :viewer="viewer" :document="document" @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 -->

View File

@@ -9,26 +9,28 @@ import {Matrix4} from 'three/src/math/Matrix4'
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */ /** This class helps manage SceneManagerData. All methods are static to support reactivity... */
export class SceneMgr { export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */ /** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(sceneUrl: Ref<string>, document: ShallowRef<Document>, name: string, url: string) { static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string): Promise<Document> {
let loadStart = performance.now(); let loadStart = performance.now();
// Start merging into the current document, replacing or adding as needed // Start merging into the current document, replacing or adding as needed
document.value = await mergePartial(url, name, document.value); document = await mergePartial(url, name, document);
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
if (name !== extrasNameValueHelpers) { if (name !== extrasNameValueHelpers) {
// Reload the helpers to fit the new model // Reload the helpers to fit the new model
await this.reloadHelpers(sceneUrl, document); document = await this.reloadHelpers(sceneUrl, document);
} else { } else {
// Display the final fully loaded model // Display the final fully loaded model
await this.showCurrentDoc(sceneUrl, document); let displayStart = performance.now();
document = await this.showCurrentDoc(sceneUrl, document);
console.log("Scene displayed in", performance.now() - displayStart, "ms");
} }
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
return document; return document;
} }
private static async reloadHelpers(sceneUrl: Ref<string>, document: ShallowRef<Document>) { private static async reloadHelpers(sceneUrl: Ref<string>, document: Document): Promise<Document> {
let bb = SceneMgr.getBoundingBox(document); let bb = SceneMgr.getBoundingBox(document);
// Create the helper axes and grid box // Create the helper axes and grid box
@@ -37,14 +39,14 @@ export class SceneMgr {
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform); newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform); newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)])); let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl); return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl);
} }
static getBoundingBox(document: ShallowRef<Document>): Box3 { static getBoundingBox(document: Document): Box3 {
// 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
let bbMin: number[] = [1e6, 1e6, 1e6]; let bbMin: number[] = [1e6, 1e6, 1e6];
let bbMax: number[] = [-1e6, -1e6, -1e6]; let bbMax: number[] = [-1e6, -1e6, -1e6];
document.value.getRoot().listNodes().forEach(node => { document.getRoot().listNodes().forEach(node => {
if ((node.getExtras()[extrasNameKey] ?? extrasNameValueHelpers) === extrasNameValueHelpers) return; if ((node.getExtras()[extrasNameKey] ?? extrasNameValueHelpers) === extrasNameValueHelpers) return;
let transform = new Matrix4(...node.getWorldMatrix()); let transform = new Matrix4(...node.getWorldMatrix());
for (let prim of node.getMesh()?.listPrimitives() ?? []) { for (let prim of node.getMesh()?.listPrimitives() ?? []) {
@@ -64,29 +66,31 @@ export class SceneMgr {
} }
/** Removes a model from the viewer */ /** Removes a model from the viewer */
static async removeModel(sceneUrl: Ref<string>, document: ShallowRef<Document>, name: string) { static async removeModel(sceneUrl: Ref<string>, document: Document, name: string): Promise<Document> {
let loadStart = performance.now(); let loadStart = performance.now();
// Remove the model from the document // Remove the model from the document
document.value = await removeModel(name, document.value) document = await removeModel(name, document)
console.log("Model", name, "removed in", performance.now() - loadStart, "ms"); console.log("Model", name, "removed in", performance.now() - loadStart, "ms");
// Reload the helpers to fit the new model (will also show the document) // Reload the helpers to fit the new model (will also show the document)
await this.reloadHelpers(sceneUrl, document); document = await this.reloadHelpers(sceneUrl, document);
return document; return document;
} }
/** 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: ShallowRef<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
document.value = await mergeFinalize(document.value); document = await mergeFinalize(document);
// 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.value); 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, "as", Array.from(buffer));
sceneUrl.value = URL.createObjectURL(blob); sceneUrl.value = URL.createObjectURL(blob);
return document;
} }
} }

View File

@@ -4,9 +4,9 @@ export const settings = {
// @ts-ignore // @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href, // new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore // @ts-ignore
// new URL('../../assets/logo.glb', import.meta.url).href, new URL('../../assets/logo.glb', import.meta.url).href,
// Websocket URLs automatically listen for new models from the python backend // Websocket URLs automatically listen for new models from the python backend
"ws://192.168.1.132:32323/" // "ws://192.168.1.132:32323/"
], ],
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
checkServerEveryMs: 100, /* How often to check for a new server */ checkServerEveryMs: 100, /* How often to check for a new server */

View File

@@ -13,7 +13,7 @@ import {
} from "vuetify/lib/components"; } from "vuetify/lib/components";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf"; import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
import {Document, Mesh} from "@gltf-transform/core"; import {Document, Mesh} from "@gltf-transform/core";
import {inject, ref, ShallowRef, watch} from "vue"; import {inject, ref, ShallowRef, watch, Ref} from "vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import { import {
mdiCircleOpacity, mdiCircleOpacity,
@@ -34,8 +34,7 @@ import {Vector3} from "three/src/math/Vector3";
const props = defineProps<{ const props = defineProps<{
meshes: Array<Mesh>, meshes: Array<Mesh>,
viewer: InstanceType<typeof ModelViewerWrapper> | null, viewer: InstanceType<typeof ModelViewerWrapper> | null
document: Document
}>(); }>();
const emit = defineEmits<{ remove: [] }>() const emit = defineEmits<{ remove: [] }>()
@@ -111,7 +110,7 @@ function onOpacityChange(newOpacity: number) {
watch(opacity, onOpacityChange); watch(opacity, onOpacityChange);
let document: ShallowRef<Document> = inject('document'); let {sceneDocument}: {sceneDocument: ShallowRef<Document>} = inject('sceneDocument');
function onClipPlanesChange() { function onClipPlanesChange() {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
@@ -126,7 +125,7 @@ function onClipPlanesChange() {
// Global value for all models, once set it cannot be unset (unknown for other models...) // Global value for all models, once set it cannot be unset (unknown for other models...)
props.viewer.renderer.threeRenderer.localClippingEnabled = true; props.viewer.renderer.threeRenderer.localClippingEnabled = true;
// Due to model-viewer's camera manipulation, the bounding box needs to be transformed // Due to model-viewer's camera manipulation, the bounding box needs to be transformed
bbox = SceneMgr.getBoundingBox(document); bbox = SceneMgr.getBoundingBox(sceneDocument.value);
bbox.applyMatrix4(sceneModel.matrixWorld); bbox.applyMatrix4(sceneModel.matrixWorld);
} }
sceneModel.traverse((child) => { sceneModel.traverse((child) => {

View File

@@ -5,16 +5,18 @@ import Loading from "../misc/Loading.vue";
import {Document, Mesh} from "@gltf-transform/core"; import {Document, Mesh} from "@gltf-transform/core";
import {extrasNameKey} from "../misc/gltf"; import {extrasNameKey} from "../misc/gltf";
import Model from "./Model.vue"; import Model from "./Model.vue";
import {ref} from "vue"; import {ref, watch, inject, Ref} from "vue";
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null, document: Document }>(); const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
const emit = defineEmits<{ remove: [string] }>() const emit = defineEmits<{ remove: [string] }>()
let {sceneDocument} = inject<{ sceneDocument: Ref<Document> }>('sceneDocument');
let expandedNames = ref<Array<string>>([]); let expandedNames = ref<Array<string>>([]);
function meshesList(document: Document): Array<Array<Mesh>> { function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
// Grouped by shared name // Grouped by shared name
return document.getRoot().listMeshes().reduce((acc, mesh) => { return sceneDocument.getRoot().listMeshes().reduce((acc, mesh) => {
let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed'; let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed';
let group = acc.find((group) => meshName(group[0]) === name); let group = acc.find((group) => meshName(group[0]) === name);
if (group) { if (group) {
@@ -44,10 +46,9 @@ defineExpose({findModel})
</script> </script>
<template> <template>
<Loading v-if="!props.document"/> <v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
<v-expansion-panels v-else v-for="meshes in meshesList(props.document)" :key="meshName(meshes[0])"
v-model="expandedNames" multiple> v-model="expandedNames" multiple>
<model :meshes="meshes" :viewer="props.viewer" :document="props.document" @remove="onRemove(meshes[0])"/> <model :meshes="meshes" :viewer="props.viewer" @remove="onRemove(meshes[0])"/>
</v-expansion-panels> </v-expansion-panels>
</template> </template>

View File

@@ -180,11 +180,11 @@ function toggleHighlightNextSelection() {
function toggleShowBoundingBox() { function toggleShowBoundingBox() {
showBoundingBox.value = !showBoundingBox.value; showBoundingBox.value = !showBoundingBox.value;
if (!firstLoad /*bug?*/) updateBoundingBox();
} }
let viewerFound = false let viewerFound = false
let firstLoad = true; let firstLoad = true;
let hasListeners = false;
let cameraChangeWaiting = false; let cameraChangeWaiting = false;
let cameraChangeLast = 0 let cameraChangeLast = 0
let onCameraChange = () => { let onCameraChange = () => {
@@ -206,29 +206,38 @@ let onCameraChange = () => {
setTimeout(waitingHandler, 100); // Wait for the camera to stop moving setTimeout(waitingHandler, 100); // Wait for the camera to stop moving
}; };
watch(() => props.viewer, (viewer) => { watch(() => props.viewer, (viewer) => {
console.log('Viewer changed', viewer)
if (!viewer) return; if (!viewer) return;
if (viewerFound) return;
viewerFound = true;
let hasListeners = false;
// 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
viewer.onElemReady((elem) => { viewer.onElemReady((elem) => {
if (viewerFound) return;
viewerFound = true;
if (hasListeners) return; if (hasListeners) return;
hasListeners = true; hasListeners = true;
elem.addEventListener('mouseup', selectionListener); elem.addEventListener('mouseup', selectionListener);
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
elem.addEventListener('load', () => { elem.addEventListener('load', () => {
console.log('Model loaded')
if (firstLoad) { if (firstLoad) {
toggleShowBoundingBox(); toggleShowBoundingBox();
firstLoad = false; firstLoad = false;
} else { }
updateBoundingBox();
});
console.log(elem)
if (elem.loaded) {
console.log('Model already loaded')
if (firstLoad) {
toggleShowBoundingBox();
firstLoad = false;
}
updateBoundingBox(); updateBoundingBox();
} }
});
elem.addEventListener('camera-change', onCameraChange); elem.addEventListener('camera-change', onCameraChange);
}); });
}); });
let document: ShallowRef<Document> = inject('document'); let {sceneDocument}: { sceneDocument: ShallowRef<Document> } = inject('sceneDocument');
let boundingBoxLines: { [points: string]: number } = {} let boundingBoxLines: { [points: string]: number } = {}
@@ -250,7 +259,7 @@ function updateBoundingBox() {
} }
bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget())); bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget()));
} else { } else {
bb = SceneMgr.getBoundingBox(document); bb = SceneMgr.getBoundingBox(sceneDocument.value);
} }
// Define each edge of the bounding box, to draw a line for each axis // Define each edge of the bounding box, to draw a line for each axis
let corners = [ let corners = [
@@ -361,8 +370,6 @@ function updateDistances() {
return; return;
} }
defineExpose({onCameraChange})
// Add keyboard shortcuts // Add keyboard shortcuts
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if (event.key === 's') { if (event.key === 's') {

View File

@@ -20,12 +20,14 @@ def build_logo() -> TopoDS_Shape:
return logo_obj.part.wrapped return logo_obj.part.wrapped
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
# Start an offline "server" to merge the CAD part of the logo with the animated GLTF part of the logo # Start an offline "server" to merge the CAD part of the logo with the animated GLTF part of the logo
os.environ['YACV_DISABLE_SERVER'] = '1' os.environ['YACV_DISABLE_SERVER'] = '1'
from yacv_server import show_object, server from yacv_server import show_object, server
ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets')) ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets'))
# Add the CAD part of the logo to the server # Add the CAD part of the logo to the server
@@ -38,6 +40,7 @@ if __name__ == "__main__":
async def writer(): async def writer():
f.write(await server.export('logo')) f.write(await server.export('logo'))
asyncio.run(writer()) asyncio.run(writer())
print('Logo saved to', os.path.join(ASSETS_DIR, 'logo.glb')) print('Logo saved to', os.path.join(ASSETS_DIR, 'logo.glb'))