mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 14:37:03 +01:00
better model updates, broken tool/selection init
This commit is contained in:
30
src/App.vue
30
src/App.vue
@@ -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 -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export type MObject3D = Object3D & {
|
||||
|
||||
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
|
||||
let emit = defineEmits<{ findModel: [string] }>();
|
||||
let {setDisableTap} = inject<{setDisableTap: (boolean) => void}>('disableTap');
|
||||
let {setDisableTap} = inject<{ setDisableTap: (boolean) => void }>('disableTap');
|
||||
let selectionEnabled = ref(false);
|
||||
let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
|
||||
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||
@@ -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,33 +370,31 @@ function updateDistances() {
|
||||
return;
|
||||
}
|
||||
|
||||
defineExpose({onCameraChange})
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 's') {
|
||||
if(selectFilter.value == 'Any (S)') toggleSelection();
|
||||
if (selectFilter.value == 'Any (S)') toggleSelection();
|
||||
else {
|
||||
selectFilter.value = 'Any (S)';
|
||||
if(!selectionEnabled.value) toggleSelection();
|
||||
if (!selectionEnabled.value) toggleSelection();
|
||||
}
|
||||
} else if (event.key === 'f') {
|
||||
if(selectFilter.value == '(F)aces') toggleSelection();
|
||||
if (selectFilter.value == '(F)aces') toggleSelection();
|
||||
else {
|
||||
selectFilter.value = '(F)aces';
|
||||
if(!selectionEnabled.value) toggleSelection();
|
||||
if (!selectionEnabled.value) toggleSelection();
|
||||
}
|
||||
} else if (event.key === 'e') {
|
||||
if(selectFilter.value == '(E)dges') toggleSelection();
|
||||
if (selectFilter.value == '(E)dges') toggleSelection();
|
||||
else {
|
||||
selectFilter.value = '(E)dges';
|
||||
if(!selectionEnabled.value) toggleSelection();
|
||||
if (!selectionEnabled.value) toggleSelection();
|
||||
}
|
||||
} else if (event.key === 'v') {
|
||||
if(selectFilter.value == '(V)ertices') toggleSelection();
|
||||
if (selectFilter.value == '(V)ertices') toggleSelection();
|
||||
else {
|
||||
selectFilter.value = '(V)ertices';
|
||||
if(!selectionEnabled.value) toggleSelection();
|
||||
if (!selectionEnabled.value) toggleSelection();
|
||||
}
|
||||
} else if (event.key === 'b') {
|
||||
toggleShowBoundingBox();
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user