// Model management from the graphics side import type {MObject3D} from "./Selection.vue"; import type {Intersection} from "three"; import {Box3} from "three"; /** Information about a single item in the selection */ export class SelectionInfo { /** The object which was (partially) selected */ object: MObject3D /** The type of the selection */ kind: 'face' | 'edge' | 'vertex' /** Start and end indices of the primitives in the geometry */ indices: [number, number] constructor(object: MObject3D, kind: 'face' | 'edge' | 'vertex', indices: [number, number]) { this.object = object; this.kind = kind; this.indices = indices; } public getKey() { return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed(); } public getBox(): Box3 { let index = this.object.geometry.index || {getX: (i: number) => i}; let pos = this.object.geometry.getAttribute('position'); let min = [Infinity, Infinity, Infinity]; let max = [-Infinity, -Infinity, -Infinity]; for (let i = this.indices[0]; i < this.indices[1]; i++) { let vertIndex = index!.getX(i); let x = pos.getX(vertIndex); let y = pos.getY(vertIndex); let z = pos.getZ(vertIndex); min[0] = Math.min(min[0], x); min[1] = Math.min(min[1], y); min[2] = Math.min(min[2], z); max[0] = Math.max(max[0], x); max[1] = Math.max(max[1], y); max[2] = Math.max(max[2], z); } return new Box3().setFromArray([...min, ...max]); } } export function hitToSelectionInfo(hit: Intersection): SelectionInfo | null { let kind = hit.object.type; if (kind == 'Mesh' || kind == 'SkinnedMesh') { let indices = hitFaceTriangleIndices(hit); if (indices === null) return null; return new SelectionInfo(hit.object, 'face', indices); } else if (kind == 'Line' || kind == 'LineSegments') { // Select raw lines, not the wide meshes representing them // This is because the indices refer to the raw lines, not the wide meshes // Furthermore, this allows better "fuzzy" raycasting logic let indices = hitEdgePointIndices(hit); if (indices === null) return null; return new SelectionInfo(hit.object, 'edge', indices); } else if (kind == 'Points') { if (hit.index === undefined) return null; return new SelectionInfo(hit.object, 'vertex', [hit.index, hit.index + 1]); } return null; } function hitFaceTriangleIndices(hit: Intersection): [number, number] | null { let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end; if (hit.faceIndex === undefined) return null; if (!faceTrianglesEnd) { // Fallback to selecting the whole imported mesh //console.log("No face_triangles_end found, selecting the whole mesh"); return [0, (hit.object.geometry.index ?? hit.object.geometry.attributes.position).count]; } else { // Normal CAD model let rawIndex = hit.faceIndex * 3; // Faces are triangles with 3 indices for (let i = 0; i < faceTrianglesEnd.length; i++) { let faceSwapIndex = faceTrianglesEnd[i] if (rawIndex < faceSwapIndex) { let start = i === 0 ? 0 : faceTrianglesEnd[i - 1]; return [start, faceTrianglesEnd[i]]; } } } return null; } function hitEdgePointIndices(hit: Intersection): [number, number] | null { let edgePointsEnd = hit?.object?.geometry?.userData?.edge_points_end; if (!edgePointsEnd || hit.index === undefined) return null; let rawIndex = hit.index; // Faces are triangles with 3 indices for (let i = 0; i < edgePointsEnd.length; i++) { let edgeSwapIndex = edgePointsEnd[i] if (rawIndex < edgeSwapIndex) { let start = i === 0 ? 0 : edgePointsEnd[i - 1]; return [start, edgePointsEnd[i]]; } } return null; } function applyColor(selInfo: SelectionInfo, colorAttribute: any, color: [number, number, number, number]): [number, number, number, number] { let index = selInfo.object.geometry.index let prevColor: [number, number, number, number] | null = null; if (colorAttribute !== undefined) { for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) { let vertIndex = index!.getX(i); if (prevColor === null) prevColor = [colorAttribute.getX(vertIndex), colorAttribute.getY(vertIndex), colorAttribute.getZ(vertIndex), colorAttribute.getW(vertIndex)]; colorAttribute.setXYZW(vertIndex, color[0], color[1], color[2], color[3]); } colorAttribute.needsUpdate = true; if (selInfo.object.userData.niceLine !== undefined) { // Need to update the color of the nice line as well let indexAttribute = selInfo.object.geometry.index!!; let allNewColors = []; for (let i = 0; i < indexAttribute.count; i++) { if (indexAttribute.getX(i) >= selInfo.indices[0] && indexAttribute.getX(i) < selInfo.indices[1]) { allNewColors.push(color[0], color[1], color[2]); } else { allNewColors.push(colorAttribute.getX(indexAttribute.getX(i)), colorAttribute.getY(indexAttribute.getX(i)), colorAttribute.getZ(indexAttribute.getX(i))); } } selInfo.object.userData.niceLine.geometry.setColors(allNewColors); for (let attribute of Object.values(selInfo.object.userData.niceLine.geometry.attributes)) { (attribute as any).needsUpdate = true; } } } else { // Fallback to tinting the whole mesh for imported models //console.log("No color attribute found, tinting the whole mesh") let tmpPrevColor = selInfo.object.material.color; prevColor = [tmpPrevColor.r, tmpPrevColor.g, tmpPrevColor.b, 1]; selInfo.object.material.color.setRGB(color[0], color[1], color[2]); selInfo.object.material.needsUpdate = true; } return prevColor!; } export function highlight(selInfo: SelectionInfo): void { // Update the color of all the triangles in the face let geometry = selInfo.object.geometry; let colorAttr = selInfo.object.geometry.getAttribute('color'); geometry.userData.savedColor = geometry.userData.savedColor || {}; geometry.userData.savedColor[selInfo.getKey()] = applyColor(selInfo, colorAttr, [1.0, 0.0, 0.0, 1.0]); } export function highlightUndo(selInfo: SelectionInfo): void { // Update the color of all the triangles in the face let geometry = selInfo.object.geometry; let colorAttr = selInfo.object.geometry.getAttribute('color'); let savedColor = geometry.userData.savedColor[selInfo.getKey()]; applyColor(selInfo, colorAttr, savedColor); delete geometry.userData.savedColor[selInfo.getKey()]; }