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 -->
<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 Loading from "./misc/Loading.vue";
import Tools from "./tools/Tools.vue";
@@ -23,25 +23,23 @@ const ModelViewerWrapper = defineAsyncComponent({
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
let sceneUrl = ref("")
let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
let document = shallowRef(new Document());
let models: Ref<InstanceType<typeof Models> | null> = ref(null)
provide('document', document);
let disableTap = ref(false);
let setDisableTap = (val: boolean) => {
disableTap.value = val;
}
const sceneUrl = ref("")
const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
const sceneDocument = shallowRef(new Document());
provide('sceneDocument', {sceneDocument});
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
const disableTap = ref(false);
const setDisableTap = (val: boolean) => disableTap.value = val;
provide('disableTap', {disableTap, setDisableTap});
async function onModelLoadRequest(model: NetworkUpdateEvent) {
await SceneMgr.loadModel(sceneUrl, document, model.name, model.url);
document.value = document.value.clone(); // Force update from this component!
sceneDocument.value = await SceneMgr.loadModel(sceneUrl, sceneDocument.value, model.name, model.url);
triggerRef(sceneDocument); // Why not triggered automatically?
}
function onModelRemoveRequest(name: string) {
SceneMgr.removeModel(sceneUrl, document, name);
document.value = document.value.clone(); // Force update from this component!
async function onModelRemoveRequest(name: string) {
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
triggerRef(sceneDocument); // Why not triggered automatically?
}
// Set up the load model event listener
@@ -76,7 +74,7 @@ async function loadModelManual() {
<svg-icon type="mdi" :path="mdiPlus"/>
</v-btn>
</template>
<models ref="models" :viewer="viewer" :document="document" @remove="onModelRemoveRequest"/>
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
</sidebar>
<!-- 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... */
export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(sceneUrl: Ref<string>, document: ShallowRef<Document>, name: string, url: string) {
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string): Promise<Document> {
let loadStart = performance.now();
// 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) {
// Reload the helpers to fit the new model
await this.reloadHelpers(sceneUrl, document);
document = await this.reloadHelpers(sceneUrl, document);
} else {
// 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;
}
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);
// Create the helper axes and grid box
@@ -37,14 +39,14 @@ export class SceneMgr {
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
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
let bbMin: 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;
let transform = new Matrix4(...node.getWorldMatrix());
for (let prim of node.getMesh()?.listPrimitives() ?? []) {
@@ -64,29 +66,31 @@ export class SceneMgr {
}
/** 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();
// 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");
// 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;
}
/** 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
document.value = await mergeFinalize(document.value);
document = await mergeFinalize(document);
// 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'});
console.debug("Showing current doc", document, "as", Array.from(buffer));
sceneUrl.value = URL.createObjectURL(blob);
return document;
}
}

View File

@@ -4,9 +4,9 @@ export const settings = {
// @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href,
// @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
"ws://192.168.1.132:32323/"
// "ws://192.168.1.132:32323/"
],
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
checkServerEveryMs: 100, /* How often to check for a new server */

View File

@@ -13,7 +13,7 @@ import {
} from "vuetify/lib/components";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
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 {
mdiCircleOpacity,
@@ -34,8 +34,7 @@ import {Vector3} from "three/src/math/Vector3";
const props = defineProps<{
meshes: Array<Mesh>,
viewer: InstanceType<typeof ModelViewerWrapper> | null,
document: Document
viewer: InstanceType<typeof ModelViewerWrapper> | null
}>();
const emit = defineEmits<{ remove: [] }>()
@@ -111,7 +110,7 @@ function onOpacityChange(newOpacity: number) {
watch(opacity, onOpacityChange);
let document: ShallowRef<Document> = inject('document');
let {sceneDocument}: {sceneDocument: ShallowRef<Document>} = inject('sceneDocument');
function onClipPlanesChange() {
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...)
props.viewer.renderer.threeRenderer.localClippingEnabled = true;
// 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);
}
sceneModel.traverse((child) => {

View File

@@ -5,16 +5,18 @@ import Loading from "../misc/Loading.vue";
import {Document, Mesh} from "@gltf-transform/core";
import {extrasNameKey} from "../misc/gltf";
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] }>()
let {sceneDocument} = inject<{ sceneDocument: Ref<Document> }>('sceneDocument');
let expandedNames = ref<Array<string>>([]);
function meshesList(document: Document): Array<Array<Mesh>> {
function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
// 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 group = acc.find((group) => meshName(group[0]) === name);
if (group) {
@@ -44,10 +46,9 @@ defineExpose({findModel})
</script>
<template>
<Loading v-if="!props.document"/>
<v-expansion-panels v-else v-for="meshes in meshesList(props.document)" :key="meshName(meshes[0])"
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
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>
</template>

View File

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

View File

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