fixes for lines, positions, distances, bounding boxes and enable tap to move

This commit is contained in:
Yeicor
2024-03-01 19:31:16 +01:00
parent d96f7df191
commit 1f9a5f375a
4 changed files with 42 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
<!--suppress SillyAssignmentJS --> <!--suppress SillyAssignmentJS -->
<script setup lang="ts"> <script setup lang="ts">
import {defineAsyncComponent, ref, Ref, shallowRef, provide} from "vue"; import {defineAsyncComponent, provide, Ref, ref, shallowRef} 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";
@@ -28,6 +28,11 @@ let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
let document = shallowRef(new Document()); let document = shallowRef(new Document());
let models: Ref<InstanceType<typeof Models> | null> = ref(null) let models: Ref<InstanceType<typeof Models> | null> = ref(null)
provide('document', document); provide('document', document);
let disableTap = ref(false);
let setDisableTap = (val: boolean) => {
disableTap.value = val;
}
provide('disableTap', {disableTap, setDisableTap});
async function onModelLoadRequest(model: NetworkUpdateEvent) { async function onModelLoadRequest(model: NetworkUpdateEvent) {
await SceneMgr.loadModel(sceneUrl, document, model.name, model.url); await SceneMgr.loadModel(sceneUrl, document, model.name, model.url);
@@ -39,10 +44,6 @@ function onModelRemoveRequest(name: string) {
document.value = document.value.clone(); // Force update from this component! document.value = document.value.clone(); // Force update from this component!
} }
function onFindModel(name: string) {
Models.value.findModel(name);
}
// 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', onModelLoadRequest); networkMgr.addEventListener('update', onModelLoadRequest);

View File

@@ -1,20 +1,27 @@
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three'; import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
import type {MObject3D} from "../tools/Selection.vue"; import type {MObject3D} from "../tools/Selection.vue";
import type { ModelScene } from '@google/model-viewer/lib/three-components/ModelScene';
function getCenterAndVertexList(obj: InstanceType<typeof MObject3D>): { function getCenterAndVertexList(obj: InstanceType<typeof MObject3D>, scene: ModelScene): {
center: Vector3, center: Vector3,
vertices: Array<Vector3> vertices: Array<Vector3>
} { } {
obj.updateMatrixWorld(); obj.updateMatrixWorld();
let pos: InterleavedBufferAttribute = obj.geometry.getAttribute('position'); let pos: InterleavedBufferAttribute = obj.geometry.getAttribute('position');
let ind: BufferAttribute = obj.geometry.index; let ind: BufferAttribute = obj.geometry.index;
if (!ind) {
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
for (let i = 0; i < pos.count; i++) {
ind.array[i] = i;
}
}
let center = new Vector3(); let center = new Vector3();
let vertices = []; let vertices = [];
for (let i = 0; i < ind.count; i++) { for (let i = 0; i < ind.count; i++) {
let index = ind.array[i]; let index = ind.array[i];
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index)); let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
// vertex = obj.localToWorld(vertex); // TODO: Bad locations due to with model viewer? vertex = scene.target.worldToLocal(obj.localToWorld(vertex));
center.add(vertex); center.add(vertex);
vertices.push(vertex); vertices.push(vertex);
} }
@@ -27,15 +34,15 @@ function getCenterAndVertexList(obj: InstanceType<typeof MObject3D>): {
* Given two THREE.Object3D objects, returns their closest and farthest vertices, and the geometric centers. * Given two THREE.Object3D objects, returns their closest and farthest vertices, and the geometric centers.
* All of them are approximated and should not be used for precise calculations. * All of them are approximated and should not be used for precise calculations.
*/ */
export function distances(a: InstanceType<typeof MObject3D>, b: InstanceType<typeof MObject3D>): { export function distances(a: InstanceType<typeof MObject3D>, b: InstanceType<typeof MObject3D>, scene: ModelScene): {
min: Array<Vector3>, min: Array<Vector3>,
center: Array<Vector3>, center: Array<Vector3>,
max: Array<Vector3> max: Array<Vector3>
} { } {
// Simplify this problem (approximate) by using the distance between each of their vertices. // Simplify this problem (approximate) by using the distance between each of their vertices.
// Find the center of each object. // Find the center of each object.
let {center: aCenter, vertices: aVertices} = getCenterAndVertexList(a); let {center: aCenter, vertices: aVertices} = getCenterAndVertexList(a, scene);
let {center: bCenter, vertices: bVertices} = getCenterAndVertexList(b); let {center: bCenter, vertices: bVertices} = getCenterAndVertexList(b, scene);
// Find the closest and farthest vertices. // Find the closest and farthest vertices.
// TODO: Compute actual min and max distances between the two objects. // TODO: Compute actual min and max distances between the two objects.

View File

@@ -21,6 +21,7 @@ export type MObject3D = Object3D & {
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>(); let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
let emit = defineEmits<{ findModel: [string] }>(); let emit = defineEmits<{ findModel: [string] }>();
let {disableTap, setDisableTap} = inject('disableTap');
let selectionEnabled = ref(false); let selectionEnabled = ref(false);
let selected = defineModel<Array<Intersection<MObject3D>>>({default: []}); let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
@@ -160,14 +161,7 @@ function toggleSelection() {
let viewer: ModelViewerElement = props.viewer?.elem; let viewer: ModelViewerElement = props.viewer?.elem;
if (!viewer) return; if (!viewer) return;
selectionEnabled.value = !selectionEnabled.value; selectionEnabled.value = !selectionEnabled.value;
if (selectionEnabled.value) { setDisableTap(selectionEnabled.value);
for (let material of selected.value) {
select(material);
}
} else {
deselectAll(false);
}
props.viewer.scene?.queueRender() // Force rerender of model-viewer
} }
function toggleHighlightNextSelection() { function toggleHighlightNextSelection() {
@@ -186,15 +180,16 @@ 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;
watch(() => props.viewer, (viewer) => { watch(() => props.viewer, (viewer) => {
if (!viewer) return; if (!viewer) return;
if (viewerFound) return; if (viewerFound) return;
viewerFound = true; viewerFound = true;
let hasListeners = false; let hasListeners = false;
let firstLoad = true;
// 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 (hasListeners) return; if (hasListeners) return;
@@ -219,12 +214,11 @@ watch(() => props.viewer, (viewer) => {
let waitingHandler: () => void; let waitingHandler: () => void;
waitingHandler = () => { waitingHandler = () => {
// Ignore also inertia // Ignore also inertia
let stillMoving = performance.now() - lastCameraChange < 100; if (performance.now() - lastCameraChange > 250) {
if (!stillMoving) {
updateBoundingBox(); updateBoundingBox();
isWaiting = false; isWaiting = false;
} else { } else {
// If the mouse is still down, wait a little more // If the camera is still moving, wait a bit more
setTimeout(waitingHandler, 100); setTimeout(waitingHandler, 100);
} }
}; };
@@ -333,6 +327,7 @@ function updateDistances() {
// Set up the line cache (for delta updates) // Set up the line cache (for delta updates)
let distanceLinesToRemove = Object.keys(distanceLines); let distanceLinesToRemove = Object.keys(distanceLines);
function ensureLine(from: Vector3, to: Vector3, text: string, color: string) { function ensureLine(from: Vector3, to: Vector3, text: string, color: string) {
console.log('ensureLine', from, to, text, color) console.log('ensureLine', from, to, text, color)
let lineCacheKey = JSON.stringify([from, to]); let lineCacheKey = JSON.stringify([from, to]);
@@ -351,7 +346,7 @@ function updateDistances() {
// Add lines (if not already added) // Add lines (if not already added)
let objA = selected.value[0].object; let objA = selected.value[0].object;
let objB = selected.value[1].object; let objB = selected.value[1].object;
let {min, center, max} = distances(objA, objB); let {min, center, max} = distances(objA, objB, props.viewer?.scene);
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange"); ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green"); ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan"); ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");

View File

@@ -3,7 +3,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {settings} from "../misc/settings"; import {settings} from "../misc/settings";
import {onMounted} from "vue"; import {onMounted, inject, Ref} from "vue";
import {$scene, $renderer} from "@google/model-viewer/lib/model-viewer-base"; import {$scene, $renderer} from "@google/model-viewer/lib/model-viewer-base";
import Loading from "../misc/Loading.vue"; import Loading from "../misc/Loading.vue";
import {ref, watch} from "vue"; import {ref, watch} from "vue";
@@ -87,7 +87,7 @@ function removeLine3D(id: number) {
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'})); scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'}));
lines.value[id].endHotspot.parentElement.remove() lines.value[id].endHotspot.parentElement.remove()
delete lines.value[id]; delete lines.value[id];
lines.value = {...lines.value}; // TODO: Trigger reactivity not working... scene.value.queueRender() // Needed to update the hotspots
} }
function onCameraChange() { function onCameraChange() {
@@ -128,14 +128,25 @@ function onElemReady(callback: (elem: ModelViewerElement) => void) {
} }
} }
function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
return Object.entries(lines);
}
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D}); defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D});
let {disableTap} = inject('disableTap');
watch(disableTap, (value) => {
// Rerender not auto triggered? This works anyway...
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" style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto" camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto"
min-camera-orbit="-Infinity 0deg 5%" disable-tap :exposure="settings.exposure" min-camera-orbit="-Infinity 0deg 5%" :disable-tap="disableTap.value" :exposure="settings.exposure"
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
:environment-image="settings.background"> :environment-image="settings.background">
@@ -146,7 +157,7 @@ defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D});
<!-- 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" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<g v-for="[lineId, line] in Object.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]" :y1="line.start2D[1]" :x2="line.end2D[0]"
:y2="line.end2D[1]" v-bind="line.lineAttrs"/> :y2="line.end2D[1]" v-bind="line.lineAttrs"/>
<rect :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4" <rect :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"