mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
163 lines
7.4 KiB
TypeScript
163 lines
7.4 KiB
TypeScript
// Model management from the graphics side
|
|
|
|
import type {MObject3D} from "./Selection.vue";
|
|
import type {Intersection} from "three";
|
|
import {Box3} from "three";
|
|
import {extrasNameKey} from "../misc/gltf";
|
|
|
|
/** 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 getObjectName() {
|
|
return this.object.userData[extrasNameKey];
|
|
}
|
|
|
|
public matches(object: MObject3D) {
|
|
return this.getObjectName() === object.userData[extrasNameKey] &&
|
|
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
|
|
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
|
|
this.kind === 'vertex' && object.type === 'Points')
|
|
}
|
|
|
|
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<MObject3D>): 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<MObject3D>): [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<MObject3D>): [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()];
|
|
} |