diff --git a/src/misc/distances.ts b/src/misc/distances.ts new file mode 100644 index 0000000..ae6beb6 --- /dev/null +++ b/src/misc/distances.ts @@ -0,0 +1,54 @@ +import {Vector3} from 'three'; +import type {MObject3D} from "../tools/Selection.vue"; + + +/** + * 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. + */ +export function distances( + a: InstanceType, b: InstanceType): { + min: Array, + center: Array, + max: Array +} { + // Simplify this problem (approximate) by using the distance between each of their vertices. + // TODO: Compute actual min and max distances between the two objects. + a.updateMatrixWorld(); + b.updateMatrixWorld(); + // FIXME: Working for points and lines, but not triangles... + const aVertices = a.geometry.getAttribute('position').array; + const aCenter = new Vector3(); + const bVertices = b.geometry.getAttribute('position').array; + const bCenter = new Vector3(); + let minDistance = Infinity; + let minDistanceVertices = [new Vector3(), new Vector3()]; + let maxDistance = -Infinity; + let maxDistanceVertices = [new Vector3(), new Vector3()]; + for (let i = 0; i < aVertices.length; i += 3) { + const v = new Vector3(aVertices[i], aVertices[i + 1], aVertices[i + 2]); + //a.localToWorld(v); + aCenter.add(v); + for (let j = 0; j < bVertices.length; j += 3) { + const w = new Vector3(bVertices[j], bVertices[j + 1], bVertices[j + 2]); + //b.localToWorld(w); + bCenter.add(w); + const d = v.distanceTo(w); + if (d < minDistance) { + minDistance = d; + minDistanceVertices = [v, w]; + } + if (d > maxDistance) { + maxDistance = d; + maxDistanceVertices = [v, w]; + } + } + } + aCenter.divideScalar(aVertices.length / 3); + bCenter.divideScalar(bVertices.length / 3); + return { + min: minDistanceVertices, + center: [aCenter, bCenter], + max: maxDistanceVertices + }; +} \ No newline at end of file diff --git a/src/tools/Selection.vue b/src/tools/Selection.vue index d62598c..ca23f41 100644 --- a/src/tools/Selection.vue +++ b/src/tools/Selection.vue @@ -12,6 +12,7 @@ import {extrasNameKey} from "../misc/gltf"; import {SceneMgr} from "../misc/scene"; import {Document} from "@gltf-transform/core"; import {AxesColors} from "../misc/helpers"; +import {distances} from "../misc/distances"; export type MObject3D = Object3D & { userData: { noHit?: boolean }, @@ -23,13 +24,12 @@ let emit = defineEmits<{ findModel: [string] }>(); let selectionEnabled = ref(false); let selected = defineModel>>({default: []}); let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before -let showBoundingBox = ref(false); +let showBoundingBox = ref(false); // Enabled automatically on start +let showDistances = ref(true); let mouseDownAt: [number, number] | null = null; let selectFilter = ref('Any'); const raycaster = new Raycaster(); -raycaster.params.Line.threshold = 0.2; -raycaster.params.Points.threshold = 0.8; let selectionMoveListener = (event: MouseEvent) => { @@ -52,6 +52,21 @@ let selectionListener = (event: MouseEvent) => { return; } + // Set raycaster parameters + if (selectFilter.value === 'Any') { + raycaster.params.Line.threshold = 0.2; + raycaster.params.Points.threshold = 0.8; + } else if(selectFilter.value === 'Edges') { + raycaster.params.Line.threshold = 0.8; + raycaster.params.Points.threshold = 0.0; + } else if (selectFilter.value === 'Vertices') { + raycaster.params.Line.threshold = 0.0; + raycaster.params.Points.threshold = 0.8; + } else if (selectFilter.value === 'Faces') { + raycaster.params.Line.threshold = 0.0; + raycaster.params.Points.threshold = 0.0; + } + // Define the 3D ray from the camera to the mouse // NOTE: Need to access internal as the API has issues with small faces surrounded by edges let scene: ModelScene = props.viewer?.scene; @@ -95,6 +110,7 @@ let selectionListener = (event: MouseEvent) => { deselectAll(); } updateBoundingBox(); + updateDistances(); } else { // Otherwise, highlight the model that owns the hit emit('findModel', hit.object.userData[extrasNameKey]) @@ -282,7 +298,6 @@ function updateBoundingBox() { let color = [AxesColors.x, AxesColors.y, AxesColors.z][edgeI][1]; // Secondary colors let lineCacheKey = JSON.stringify([from, to]); let matchingLine = boundingBoxLines[lineCacheKey]; - console.log('Edge', edge, 'Matching line', matchingLine, 'key') if (matchingLine) { boundingBoxLinesToRemove = boundingBoxLinesToRemove.filter((l) => l !== lineCacheKey); } else { @@ -299,6 +314,56 @@ function updateBoundingBox() { delete boundingBoxLines[lineLocator]; } } + +function toggleShowDistances() { + showDistances.value = !showDistances.value; + updateDistances(); +} + +let distanceLines: { [points: string]: number } = {} + +function updateDistances() { + if (!showDistances.value || selected.value.length != 2) { + for (let lineId of Object.values(distanceLines)) { + props.viewer?.removeLine3D(lineId); + } + distanceLines = {}; + return; + } + + // Set up the line cache (for delta updates) + let distanceLinesToRemove = Object.keys(distanceLines); + function ensureLine(from: Vector3, to: Vector3, text: string, color: string) { + console.log('ensureLine', from, to, text, color) + let lineCacheKey = JSON.stringify([from, to]); + let matchingLine = distanceLines[lineCacheKey]; + if (matchingLine) { + distanceLinesToRemove = distanceLinesToRemove.filter((l) => l !== lineCacheKey); + } else { + distanceLines[lineCacheKey] = props.viewer?.addLine3D(from, to, text, { + "stroke": color, + "stroke-width": "2", + "stroke-dasharray": "5" + }); + } + } + + // Add lines (if not already added) + let objA = selected.value[0].object; + let objB = selected.value[1].object; + let {min, center, max} = distances(objA, objB); + 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(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan"); + + // Remove the lines that are no longer needed + for (let lineLocator of distanceLinesToRemove) { + props.viewer?.removeLine3D(distanceLines[lineLocator]); + delete distanceLines[lineLocator]; + } + + return; +}