add the ability to expand models by clicking on them

This commit is contained in:
Yeicor
2024-02-25 21:22:14 +01:00
parent 16155e7db5
commit 20b785a89a
5 changed files with 112 additions and 33 deletions

View File

@@ -4,10 +4,11 @@ import {VBtn, VSelect, VTooltip} from "vuetify/lib/components";
import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue';
import type {ModelViewerElement} from '@google/model-viewer';
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import {mdiCursorDefaultClick} from '@mdi/js';
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
import type {Intersection, Material, Object3D} from "three";
import {Raycaster} from "three";
import type ModelViewerWrapperT from "./ModelViewerWrapper.vue";
import {extrasNameKey} from "../misc/gltf";
export type MObject3D = Object3D & {
userData: { noHit?: boolean },
@@ -15,12 +16,16 @@ export type MObject3D = Object3D & {
};
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
let emit = defineEmits<{ findModel: [string] }>();
let selectionEnabled = ref(false);
let selectedMaterials = 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 showBoundingBox = ref<Boolean>(false);
let hasListener = false;
let mouseDownAt: [number, number] | null = null;
let selectFilter = ref('Any');
const ray_caster = new Raycaster();
const raycaster = new Raycaster();
let selectionMoveListener = (event: MouseEvent) => {
if (!selectionEnabled.value) return;
@@ -44,15 +49,15 @@ let selectionListener = (event: MouseEvent) => {
// NOTE: Need to access internal as the API has issues with small faces surrounded by edges
const ndcCoords = scene.getNDC(event.clientX, event.clientY);
//const hit = scene.hitFromPoint(ndcCoords) as Intersection<MObject3D> | undefined;
ray_caster.setFromCamera(ndcCoords, (scene as any).camera);
raycaster.setFromCamera(ndcCoords, (scene as any).camera);
if ((scene as any).camera.isOrthographicCamera) {
// Need to fix the ray direction for ortho camera
// FIXME: Still buggy (but less so :)
ray_caster.ray.direction.copy(
raycaster.ray.direction.copy(
scene.getTarget().clone().add(scene.target.position).sub((scene as any).camera.position).normalize());
}
// console.log('NDC', ndcCoords, 'Camera', (scene as any).camera, 'Ray', ray_caster.ray);
const hits = ray_caster.intersectObject(scene, true);
const hits = raycaster.intersectObject(scene, true);
let hit = hits.find((hit) => {
const kind = hit.object.type
const kindOk = (selectFilter.value === 'Any') ||
@@ -62,24 +67,31 @@ let selectionListener = (event: MouseEvent) => {
return hit.object.visible && !hit.object.userData.noHit && kindOk;
}) as Intersection<MObject3D> | undefined;
console.log('Hit', hit)
if (!hit) {
deselectAll();
} else {
// Toggle selection
const wasSelected = selectedMaterials.value.find((m) => m.object.name === hit.object.name) !== undefined;
if (wasSelected) {
deselect(hit)
if (!highlightNextSelection.value[0]) {
if (!hit) {
deselectAll();
} else {
select(hit)
// Toggle selection
const wasSelected = selected.value.find((m) => m.object.name === hit.object.name) !== undefined;
if (wasSelected) {
deselect(hit)
} else {
select(hit)
}
}
} else {
// Otherwise, highlight the model that owns the hit
emit('findModel', hit.object.userData[extrasNameKey])
// And reset the selection mode
toggleHighlightNextSelection()
}
scene.queueRender() // Force rerender of model-viewer
};
}
function select(hit: Intersection<MObject3D>) {
console.log('Selecting', hit.object.name)
if (selectedMaterials.value.find((m) => m.object.name === hit.object.name) === undefined) {
selectedMaterials.value.push(hit);
if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) {
selected.value.push(hit);
}
hit.object.material.__prevBaseColorFactor = [
hit.object.material.color.r,
@@ -95,8 +107,8 @@ function deselect(hit: Intersection<MObject3D>, alsoRemove = true) {
console.log('Deselecting', hit.object.name)
if (alsoRemove) {
// Remove the matching object from the selection
let toRemove = selectedMaterials.value.findIndex((m) => m.object.name === hit.object.name);
selectedMaterials.value.splice(toRemove, 1);
let toRemove = selected.value.findIndex((m) => m.object.name === hit.object.name);
selected.value.splice(toRemove, 1);
}
hit.object.material.color.r = hit.object.material.__prevBaseColorFactor[0]
hit.object.material.color.g = hit.object.material.__prevBaseColorFactor[1]
@@ -106,7 +118,7 @@ function deselect(hit: Intersection<MObject3D>, alsoRemove = true) {
function deselectAll(alsoRemove = true) {
// Clear selection (shallow copy to avoid modifying the array while iterating)
let toClear = selectedMaterials.value.slice();
let toClear = selected.value.slice();
for (let material of toClear) {
deselect(material, alsoRemove);
}
@@ -122,7 +134,7 @@ function toggleSelection() {
viewer.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
hasListener = true;
}
for (let material of selectedMaterials.value) {
for (let material of selected.value) {
select(material);
}
} else {
@@ -130,23 +142,69 @@ function toggleSelection() {
}
props.viewer.scene.queueRender() // Force rerender of model-viewer
}
function toggleHighlightNextSelection() {
highlightNextSelection.value = [
!highlightNextSelection.value[0],
highlightNextSelection.value[0] ? highlightNextSelection.value[1] : selectionEnabled.value
];
if (highlightNextSelection.value[0]) {
// Reuse selection code to identify the model
if (!selectionEnabled.value) toggleSelection()
} else {
if (selectionEnabled.value !== highlightNextSelection.value[1]) toggleSelection()
highlightNextSelection.value = [false, false];
}
}
function toggleShowBoundingBox() {
// let scene: ModelScene = props.viewer?.scene;
// if (!scene) return;
showBoundingBox.value = !showBoundingBox.value;
// scene.model?.traverse((child) => {
// if (child.userData[extrasNameKey] === modelName) {
// if (child.type === 'BoxHelper') {
// child.visible = showBoundingBox.value;
// }
// }
// });
// scene.queueRender() // Force rerender of model-viewer
}
</script>
<template>
<div class="select-parent">
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable Selection Mode' : 'Enable Selection Mode' }}
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable selection mode' : 'Enable selection mode' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiCursorDefaultClick"/>
</v-btn>
<v-tooltip :text="'Select Only ' + selectFilter" :open-on-click="false">
<v-tooltip :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()" :open-on-click="false">
<template v-slot:activator="{ props }">
<!-- TODO: Keyboard shortcuts for fast selection (& other tools) -->
<v-select v-bind="props" class="select-only" variant="underlined" :items="['Any', 'Faces', 'Edges', 'Vertices']"
<v-select v-bind="props" class="select-only" variant="underlined"
:items="['Any', 'Faces', 'Edges', 'Vertices']"
v-model="selectFilter"/>
</template>
</v-tooltip>
</div>
<v-btn icon @click="toggleHighlightNextSelection" :color="highlightNextSelection[0] ? 'surface-light' : ''">
<v-tooltip activator="parent">Highlight the next clicked element in the models list</v-tooltip>
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
</v-btn>
<!-- TODO: Show BB -->
<v-btn icon disabled @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection bounds' : 'Show selection bounds' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiCubeOutline"/>
</v-btn>
<!-- TODO: Show distances of selections (min/center/max distance) -->
<v-btn icon disabled @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection dimensions' : 'Show selection dimensions' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiRuler"/>
</v-btn>
</template>
<style scoped>