From 6a0aa265b6ad73ebd5066cb9f89df36887183cdc Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sun, 31 Aug 2025 15:15:10 +0000
Subject: [PATCH] chore(deps): update dependency @vue/tsconfig to ^0.8.0 (#251)
* chore(deps): update dependency @vue/tsconfig to ^0.8.0
* Fix new ts issues
* Add null checks for selection and model objects throughout frontend
This improves robustness by handling cases where selection or model objects may be missing or undefined, preventing
runtime errors.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+yeicor@users.noreply.github.com>
---
frontend/App.vue | 7 +-
frontend/misc/distances.ts | 135 ++++++------
frontend/misc/gltf.ts | 217 ++++++++++---------
frontend/misc/helpers.ts | 286 +++++++++++++++----------
frontend/misc/settings.ts | 2 +-
frontend/models/Model.vue | 10 +-
frontend/models/Models.vue | 6 +-
frontend/tools/Selection.vue | 46 ++--
frontend/tools/selection.ts | 283 ++++++++++++------------
frontend/viewer/ModelViewerWrapper.vue | 16 +-
frontend/viewer/lighting.ts | 140 ++++++------
package.json | 2 +-
yarn.lock | 8 +-
13 files changed, 650 insertions(+), 508 deletions(-)
diff --git a/frontend/App.vue b/frontend/App.vue
index 81775ac..eb73efc 100644
--- a/frontend/App.vue
+++ b/frontend/App.vue
@@ -51,6 +51,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
for (let modelIndex in event.models) {
let isLast = parseInt(modelIndex) === event.models.length - 1;
let model = event.models[modelIndex];
+ if (!model) continue;
tools.value?.removeObjectSelections(model.name);
try {
let loadHelpers = (await settings).loadHelpers;
@@ -153,7 +154,9 @@ document.body.addEventListener("drop", async e => {
Still trying to load the following:
- {{ model }},
+
+ {{ model }},
+
@@ -190,4 +193,4 @@ html, body {
height: 100%;
overflow: hidden !important;
}
-
\ No newline at end of file
+
diff --git a/frontend/misc/distances.ts b/frontend/misc/distances.ts
index 96f815f..c5cf16f 100644
--- a/frontend/misc/distances.ts
+++ b/frontend/misc/distances.ts
@@ -1,75 +1,88 @@
-import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
-import type {MObject3D} from "../tools/Selection.vue";
-import type {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene';
-import type {SelectionInfo} from "../tools/selection";
+import { BufferAttribute, InterleavedBufferAttribute, Vector3 } from "three";
+import type { MObject3D } from "../tools/Selection.vue";
+import type { ModelScene } from "@google/model-viewer/lib/three-components/ModelScene";
+import type { SelectionInfo } from "../tools/selection";
-
-function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
- center: Vector3,
- vertices: Array
+function getCenterAndVertexList(
+ selInfo: SelectionInfo,
+ scene: ModelScene,
+): {
+ center: Vector3;
+ vertices: Array;
} {
- let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
- let ind: BufferAttribute | null = selInfo.object.geometry.index;
- if (ind === null) {
- ind = new BufferAttribute(new Uint16Array(pos.count), 1);
- for (let i = 0; i < pos.count; i++) {
- ind.array[i] = i;
- }
+ if (!selInfo.object?.geometry) {
+ throw new Error("selInfo.object or geometry is undefined");
+ }
+ let pos = selInfo.object.geometry.getAttribute("position");
+ let ind = selInfo.object.geometry.index;
+ if (ind === null) {
+ 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 vertices = [];
- for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
- let index = ind.getX(i)
- let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
- vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
- center.add(vertex);
- vertices.push(vertex);
- }
- center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
- return {center, vertices};
+ }
+ let center = new Vector3();
+ let vertices = [];
+ for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
+ let index = ind.getX(i);
+ let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
+ vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
+ center.add(vertex);
+ vertices.push(vertex);
+ }
+ center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
+ return { center, vertices };
}
/**
* 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: SelectionInfo, b: SelectionInfo, scene: ModelScene): {
- min: Array,
- center: Array,
- max: Array
+export function distances(
+ a: SelectionInfo,
+ b: SelectionInfo,
+ scene: ModelScene,
+): {
+ min: Array;
+ center: Array;
+ max: Array;
} {
- // Simplify this problem (approximate) by using the distance between each of their vertices.
- // Find the center of each object.
- let {center: aCenter, vertices: aVertices} = getCenterAndVertexList(a, scene);
- let {center: bCenter, vertices: bVertices} = getCenterAndVertexList(b, scene);
+ // Simplify this problem (approximate) by using the distance between each of their vertices.
+ // Find the center of each object.
+ let { center: aCenter, vertices: aVertices } = getCenterAndVertexList(a, scene);
+ let { center: bCenter, vertices: bVertices } = getCenterAndVertexList(b, scene);
- // Find the closest and farthest vertices.
- // TODO: Compute actual min and max distances between the two objects.
- // FIXME: Really slow... (use a BVH or something)
- 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++) {
- for (let j = 0; j < bVertices.length; j++) {
- let distance = aVertices[i].distanceTo(bVertices[j]);
- if (distance < minDistance) {
- minDistance = distance;
- minDistanceVertices[0] = aVertices[i];
- minDistanceVertices[1] = bVertices[j];
- }
- if (distance > maxDistance) {
- maxDistance = distance;
- maxDistanceVertices[0] = aVertices[i];
- maxDistanceVertices[1] = bVertices[j];
- }
+ // Find the closest and farthest vertices.
+ // TODO: Compute actual min and max distances between the two objects.
+ // FIXME: Really slow... (use a BVH or something)
+ 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++) {
+ for (let j = 0; j < bVertices.length; j++) {
+ const aVertex = aVertices[i];
+ const bVertex = bVertices[j];
+ if (aVertex && bVertex) {
+ let distance = aVertex.distanceTo(bVertex);
+ if (distance < minDistance) {
+ minDistance = distance;
+ minDistanceVertices[0] = aVertex;
+ minDistanceVertices[1] = bVertex;
}
+ if (distance > maxDistance) {
+ maxDistance = distance;
+ maxDistanceVertices[0] = aVertex;
+ maxDistanceVertices[1] = bVertex;
+ }
+ }
}
+ }
- // Return the results.
- return {
- min: minDistanceVertices,
- center: [aCenter, bCenter],
- max: maxDistanceVertices
- };
-}
\ No newline at end of file
+ // Return the results.
+ return {
+ min: minDistanceVertices,
+ center: [aCenter, bCenter],
+ max: maxDistanceVertices,
+ };
+}
diff --git a/frontend/misc/gltf.ts b/frontend/misc/gltf.ts
index 81516a4..1555922 100644
--- a/frontend/misc/gltf.ts
+++ b/frontend/misc/gltf.ts
@@ -1,13 +1,13 @@
-import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
-import {mergeDocuments, unpartition} from "@gltf-transform/functions";
-import {retrieveFile} from "../tools/upload-file.ts";
+import { Buffer, Document, Scene, type Transform, WebIO } from "@gltf-transform/core";
+import { mergeDocuments, unpartition } from "@gltf-transform/functions";
+import { retrieveFile } from "../tools/upload-file.ts";
let io = new WebIO();
export let extrasNameKey = "__yacv_name";
export let extrasNameValueHelpers = "__helpers";
// @ts-expect-error
-let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
+let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== "undefined" && __YACV_SMALL_BUILD__;
/**
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
@@ -16,133 +16,148 @@ let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_B
*
* Remember to call mergeFinalize after all models have been merged (slower required operations).
*/
-export async function mergePartial(url: string | Blob, name: string, document: Document, networkFinished: () => void = () => {
-}): Promise {
- // Fetch the complete document from the network
- // This could be done at the same time as the document is being processed, but I wanted better metrics
- let response = await fetchOrRead(url);
- let buffer = await response.arrayBuffer();
- networkFinished();
+export async function mergePartial(
+ url: string | Blob,
+ name: string,
+ document: Document,
+ networkFinished: () => void = () => {},
+): Promise {
+ // Fetch the complete document from the network
+ // This could be done at the same time as the document is being processed, but I wanted better metrics
+ let response = await fetchOrRead(url);
+ let buffer = await response.arrayBuffer();
+ networkFinished();
- // Load the new document
- let newDoc = null;
- let alreadyTried: { [name: string]: boolean } = {}
- while (newDoc == null) { // Retry adding extensions as required until the document is loaded
- try { // Try to load fast if no extensions are used
- newDoc = await io.readBinary(new Uint8Array(buffer));
- } catch (e) { // Fallback to wait for download and register big extensions
- if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
- if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
- // WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
- let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
- // @ts-expect-error
- let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
- // @ts-expect-error
- let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
- io.registerExtensions([KHRDracoMeshCompression])
- .registerDependencies({
- 'draco3d.decoder': await dracoDecoderWeb.default({}),
- 'draco3d.encoder': await dracoEncoderWeb.default({})
- });
- } else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
- if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
- let {EXTTextureWebP} = await import("@gltf-transform/extensions")
- io.registerExtensions([EXTTextureWebP]);
- } else { // TODO: Add more extensions as required
- throw e;
- }
- }
+ // Load the new document
+ let newDoc = null;
+ let alreadyTried: { [name: string]: boolean } = {};
+ while (newDoc == null) {
+ // Retry adding extensions as required until the document is loaded
+ try {
+ // Try to load fast if no extensions are used
+ newDoc = await io.readBinary(new Uint8Array(buffer));
+ } catch (e) {
+ // Fallback to wait for download and register big extensions
+ if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
+ if (alreadyTried["draco"]) throw e;
+ else alreadyTried["draco"] = true;
+ // WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
+ let { KHRDracoMeshCompression } = await import("@gltf-transform/extensions");
+ // @ts-expect-error
+ let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
+ // @ts-expect-error
+ let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
+ io.registerExtensions([KHRDracoMeshCompression]).registerDependencies({
+ "draco3d.decoder": await dracoDecoderWeb.default({}),
+ "draco3d.encoder": await dracoEncoderWeb.default({}),
+ });
+ } else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
+ if (alreadyTried["webp"]) throw e;
+ else alreadyTried["webp"] = true;
+ let { EXTTextureWebP } = await import("@gltf-transform/extensions");
+ io.registerExtensions([EXTTextureWebP]);
+ } else {
+ // TODO: Add more extensions as required
+ throw e;
+ }
}
+ }
- // Remove any previous model with the same name
- await document.transform(dropByName(name));
+ // Remove any previous model with the same name
+ await document.transform(dropByName(name));
- // Ensure consistent names
- // noinspection TypeScriptValidateJSTypes
- await newDoc.transform(setNames(name));
+ // Ensure consistent names
+ // noinspection TypeScriptValidateJSTypes
+ await newDoc.transform(setNames(name));
- // Merge the new document into the current one
- mergeDocuments(document, newDoc);
- return document;
+ // Merge the new document into the current one
+ mergeDocuments(document, newDoc);
+ return document;
}
export async function mergeFinalize(document: Document): Promise {
- // Single scene & buffer required before loading & rendering
- return await document.transform(mergeScenes(), unpartition());
+ // Single scene & buffer required before loading & rendering
+ return await document.transform(mergeScenes(), unpartition());
}
export async function toBuffer(doc: Document): Promise {
- return io.writeBinary(doc);
+ return io.writeBinary(doc);
}
export async function removeModel(name: string, document: Document): Promise {
- return await document.transform(dropByName(name));
+ return await document.transform(dropByName(name));
}
/** Given a parsed GLTF document and a name, it forces the names of all elements to be identified by the name (or derivatives) */
function setNames(name: string): Transform {
- return (doc: Document) => {
- // Do this automatically for all elements changing any name
- for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
- if (!elem.getExtras()) elem.setExtras({});
- elem.getExtras()[extrasNameKey] = name;
- }
- return doc;
+ return (doc: Document) => {
+ // Do this automatically for all elements changing any name
+ for (let elem of doc
+ .getGraph()
+ .listEdges()
+ .map((e) => e.getChild())) {
+ if (!elem.getExtras()) elem.setExtras({});
+ elem.getExtras()[extrasNameKey] = name;
}
+ return doc;
+ };
}
/** Ensures that all elements with the given name are removed from the document */
function dropByName(name: string): Transform {
- return (doc: Document) => {
- for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
- if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
- if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
- elem.dispose();
- }
- }
- return doc;
- };
+ return (doc: Document) => {
+ for (let elem of doc
+ .getGraph()
+ .listEdges()
+ .map((e) => e.getChild())) {
+ if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
+ if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
+ elem.dispose();
+ }
+ }
+ return doc;
+ };
}
-
/** Merges all scenes in the document into a single default scene */
function mergeScenes(): Transform {
- return (doc: Document) => {
- let root = doc.getRoot();
- let scene = root.getDefaultScene() ?? root.listScenes()[0];
- for (let dropScene of root.listScenes()) {
- if (dropScene === scene) continue;
- for (let node of dropScene.listChildren()) {
- scene.addChild(node);
- }
- dropScene.dispose();
- }
+ return (doc: Document) => {
+ let root = doc.getRoot();
+ let scene = root.getDefaultScene() ?? root.listScenes()[0];
+ if (!scene) {
+ throw new Error("No scene found in GLTF document");
}
+ for (let dropScene of root.listScenes()) {
+ if (dropScene === scene) continue;
+ for (let node of dropScene.listChildren()) {
+ scene.addChild(node);
+ }
+ dropScene.dispose();
+ }
+ };
}
/** Fetches a URL or reads it if it is a Blob URL */
async function fetchOrRead(url: string | Blob) {
- if (url instanceof Blob) {
- // Use the FileReader API as fetch does not support Blob URLs
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onloadend = (event: ProgressEvent) => {
- if (event.target && event.target.result) {
- resolve(new Response(event.target.result));
- } else {
- reject(new Error("Failed to read Blob URL: " + url));
- }
- };
- reader.onerror = (error) => {
- reject(new Error("Error reading Blob URL: " + url + " - " + error));
- };
- // Read the Blob URL as an ArrayBuffer
- reader.readAsArrayBuffer(new Blob([url]));
- });
- } else {
- // Fetch the URL
- return retrieveFile(url);
- }
-
+ if (url instanceof Blob) {
+ // Use the FileReader API as fetch does not support Blob URLs
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = (event: ProgressEvent) => {
+ if (event.target && event.target.result) {
+ resolve(new Response(event.target.result));
+ } else {
+ reject(new Error("Failed to read Blob URL: " + url));
+ }
+ };
+ reader.onerror = (error) => {
+ reject(new Error("Error reading Blob URL: " + url + " - " + error));
+ };
+ // Read the Blob URL as an ArrayBuffer
+ reader.readAsArrayBuffer(new Blob([url]));
+ });
+ } else {
+ // Fetch the URL
+ return retrieveFile(url);
+ }
}
-
diff --git a/frontend/misc/helpers.ts b/frontend/misc/helpers.ts
index 1f8f526..5c4be36 100644
--- a/frontend/misc/helpers.ts
+++ b/frontend/misc/helpers.ts
@@ -1,79 +1,125 @@
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
-import {Document, type TypedArray} from '@gltf-transform/core'
-import {Vector2} from "three/src/math/Vector2.js"
-import {Vector3} from "three/src/math/Vector3.js"
-import {Matrix4} from "three/src/math/Matrix4.js"
-
+import { Document, type TypedArray } from "@gltf-transform/core";
+import { Vector2 } from "three/src/math/Vector2.js";
+import { Vector3 } from "three/src/math/Vector3.js";
+import { Matrix4 } from "three/src/math/Matrix4.js";
/** Exports the colors used for the axes, primary and secondary. They match the orientation gizmo. */
export const AxesColors = {
- x: [[247, 60, 60], [148, 36, 36]],
- z: [[108, 203, 38], [65, 122, 23]],
- y: [[23, 140, 240], [14, 84, 144]]
-}
+ x: [
+ [247, 60, 60],
+ [148, 36, 36],
+ ],
+ z: [
+ [108, 203, 38],
+ [65, 122, 23],
+ ],
+ y: [
+ [23, 140, 240],
+ [14, 84, 144],
+ ],
+};
-function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: number[], rawColors: number[] | null, transform: Matrix4, name: string = '__helper', mode: number = WebGL2RenderingContext.LINES) {
- const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + 'Buffer')
- const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + 'Scene')
- const positions = doc.createAccessor(name + 'Position')
- .setArray(new Float32Array(rawPositions) as TypedArray)
- .setType('VEC3')
- .setBuffer(buffer)
- const indices = doc.createAccessor(name + 'Indices')
- .setArray(new Uint32Array(rawIndices) as TypedArray)
- .setType('SCALAR')
- .setBuffer(buffer)
- let colors = null;
- if (rawColors) {
- colors = doc.createAccessor(name + 'Color')
- .setArray(new Float32Array(rawColors) as TypedArray)
- .setType('VEC4')
- .setBuffer(buffer);
- }
- const material = doc.createMaterial(name + 'Material')
- .setAlphaMode('OPAQUE')
- const geometry = doc.createPrimitive()
- .setIndices(indices)
- .setAttribute('POSITION', positions)
- .setMode(mode as any)
- .setMaterial(material)
- if (rawColors) {
- geometry.setAttribute('COLOR_0', colors)
- }
- if (mode == WebGL2RenderingContext.TRIANGLES) {
- geometry.setExtras({face_triangles_end: [rawIndices.length / 6, rawIndices.length * 2 / 6, rawIndices.length * 3 / 6, rawIndices.length * 4 / 6, rawIndices.length * 5 / 6, rawIndices.length]})
- } else if (mode == WebGL2RenderingContext.LINES) {
- geometry.setExtras({edge_points_end: [rawIndices.length / 3, rawIndices.length * 2 / 3, rawIndices.length]})
- }
- const mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
- const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
- scene.addChild(node)
+function buildSimpleGltf(
+ doc: Document,
+ rawPositions: number[],
+ rawIndices: number[],
+ rawColors: number[] | null,
+ transform: Matrix4,
+ name: string = "__helper",
+ mode: number = WebGL2RenderingContext.LINES,
+) {
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + "Buffer");
+ const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + "Scene");
+ if (!scene) throw new Error("Scene is undefined");
+ if (!rawPositions) throw new Error("rawPositions is undefined");
+ const positions = doc
+ .createAccessor(name + "Position")
+ .setArray(new Float32Array(rawPositions) as TypedArray)
+ .setType("VEC3")
+ .setBuffer(buffer);
+ const indices = doc
+ .createAccessor(name + "Indices")
+ .setArray(new Uint32Array(rawIndices) as TypedArray)
+ .setType("SCALAR")
+ .setBuffer(buffer);
+ let colors = null;
+ if (rawColors) {
+ colors = doc
+ .createAccessor(name + "Color")
+ .setArray(new Float32Array(rawColors) as TypedArray)
+ .setType("VEC4")
+ .setBuffer(buffer);
+ }
+ const material = doc.createMaterial(name + "Material").setAlphaMode("OPAQUE");
+ const geometry = doc
+ .createPrimitive()
+ .setIndices(indices)
+ .setAttribute("POSITION", positions)
+ .setMode(mode as any)
+ .setMaterial(material);
+ if (rawColors) {
+ geometry.setAttribute("COLOR_0", colors);
+ }
+ if (mode == WebGL2RenderingContext.TRIANGLES) {
+ geometry.setExtras({
+ face_triangles_end: [
+ rawIndices.length / 6,
+ (rawIndices.length * 2) / 6,
+ (rawIndices.length * 3) / 6,
+ (rawIndices.length * 4) / 6,
+ (rawIndices.length * 5) / 6,
+ rawIndices.length,
+ ],
+ });
+ } else if (mode == WebGL2RenderingContext.LINES) {
+ geometry.setExtras({ edge_points_end: [rawIndices.length / 3, (rawIndices.length * 2) / 3, rawIndices.length] });
+ }
+ const mesh = doc.createMesh(name + "Mesh").addPrimitive(geometry);
+ const node = doc
+ .createNode(name + "Node")
+ .setMesh(mesh)
+ .setMatrix(transform.elements as any);
+ scene.addChild(node);
}
/**
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
*/
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
- let rawIndices = [0, 1, 2, 3, 4, 5];
- let rawPositions = [
- 0, 0, 0, size.x, 0, 0,
- 0, 0, 0, 0, size.y, 0,
- 0, 0, 0, 0, 0, -size.z,
- ];
- let rawColors = [
- ...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
- ...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
- ...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
- ].map(x => x / 255.0);
- // Axes at (0, 0, 0)
- buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
- buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
- // Axes at center
- if (new Matrix4() != transform) {
- buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes_center');
- buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes_center', WebGL2RenderingContext.POINTS);
- }
+ let rawIndices = [0, 1, 2, 3, 4, 5];
+ let rawPositions = [0, 0, 0, size.x, 0, 0, 0, 0, 0, 0, size.y, 0, 0, 0, 0, 0, 0, -size.z];
+ let rawColors = [
+ ...(AxesColors.x[0] ?? [255, 0, 0]),
+ 255,
+ ...(AxesColors.x[1] ?? [255, 0, 0]),
+ 255,
+ ...(AxesColors.y[0] ?? [0, 255, 0]),
+ 255,
+ ...(AxesColors.y[1] ?? [0, 255, 0]),
+ 255,
+ ...(AxesColors.z[0] ?? [0, 0, 255]),
+ 255,
+ ...(AxesColors.z[1] ?? [0, 0, 255]),
+ 255,
+ ].map((x) => x / 255.0);
+ // Axes at (0, 0, 0)
+ buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), "__helper_axes");
+ buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), "__helper_axes", WebGL2RenderingContext.POINTS);
+ // Axes at center
+ if (new Matrix4() != transform) {
+ buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, "__helper_axes_center");
+ buildSimpleGltf(
+ doc,
+ [0, 0, 0],
+ [0],
+ [1, 1, 1, 1],
+ transform,
+ "__helper_axes_center",
+ WebGL2RenderingContext.POINTS,
+ );
+ }
}
/**
@@ -83,61 +129,69 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
* This ensures that only the back of the grid is always visible, regardless of the camera position.
*/
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
- // Create transformed positions for the inner faces of the box
- let allPositions: number[] = [];
- let allIndices: number[] = [];
- for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
- for (let positive of [1, -1]) {
- let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
- let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z)
- let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0))
- let size2 = new Vector2();
- if (axis.x) size2.set(size.z, size.y);
- if (axis.y) size2.set(size.x, size.z);
- if (axis.z) size2.set(size.x, size.y);
- let transform = new Matrix4().multiply(translation).multiply(rotation);
- let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
- let baseIndex = allPositions.length / 3;
- for (let i of rawIndices) {
- allIndices.push(i + baseIndex);
- }
- // Apply transform to the positions before adding them to the list
- for (let i = 0; i < rawPositions.length; i += 3) {
- let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
- pos.applyMatrix4(transform);
- allPositions.push(pos.x, pos.y, pos.z);
- }
- }
+ // Create transformed positions for the inner faces of the box
+ let allPositions: number[] = [];
+ let allIndices: number[] = [];
+ for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
+ for (let positive of [1, -1]) {
+ let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
+ let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
+ let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0));
+ let size2 = new Vector2();
+ if (axis.x) size2.set(size.z, size.y);
+ if (axis.y) size2.set(size.x, size.z);
+ if (axis.z) size2.set(size.x, size.y);
+ let transform = new Matrix4().multiply(translation).multiply(rotation);
+ let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
+ let baseIndex = allPositions.length / 3;
+ for (let i of rawIndices) {
+ allIndices.push(i + baseIndex);
+ }
+ // Apply transform to the positions before adding them to the list
+ for (let i = 0; i < rawPositions.length; i += 3) {
+ let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
+ pos.applyMatrix4(transform);
+ allPositions.push(pos.x, pos.y, pos.z);
+ }
}
- let colors = new Array(allPositions.length / 3 * 4).fill(1);
- buildSimpleGltf(doc, allPositions, allIndices, colors, baseTransform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
+ }
+ let colors = new Array((allPositions.length / 3) * 4).fill(1);
+ buildSimpleGltf(
+ doc,
+ allPositions,
+ allIndices,
+ colors,
+ baseTransform,
+ "__helper_grid",
+ WebGL2RenderingContext.TRIANGLES,
+ );
}
export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] {
- const rawPositions = [];
- const rawIndices = [];
- // Build the grid as triangles
- for (let i = 0; i <= divisions; i++) {
- const x = -size.x / 2 + size.x * i / divisions;
- const y = -size.y / 2 + size.y * i / divisions;
+ const rawPositions = [];
+ const rawIndices = [];
+ // Build the grid as triangles
+ for (let i = 0; i <= divisions; i++) {
+ const x = -size.x / 2 + (size.x * i) / divisions;
+ const y = -size.y / 2 + (size.y * i) / divisions;
- // Vertical quad (two triangles)
- rawPositions.push(x - divisionWidth * size.x / 2, -size.y / 2, 0);
- rawPositions.push(x + divisionWidth * size.x / 2, -size.y / 2, 0);
- rawPositions.push(x + divisionWidth * size.x / 2, size.y / 2, 0);
- rawPositions.push(x - divisionWidth * size.x / 2, size.y / 2, 0);
- const baseIndex = i * 4;
- rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
- rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
+ // Vertical quad (two triangles)
+ rawPositions.push(x - (divisionWidth * size.x) / 2, -size.y / 2, 0);
+ rawPositions.push(x + (divisionWidth * size.x) / 2, -size.y / 2, 0);
+ rawPositions.push(x + (divisionWidth * size.x) / 2, size.y / 2, 0);
+ rawPositions.push(x - (divisionWidth * size.x) / 2, size.y / 2, 0);
+ const baseIndex = i * 4;
+ rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
+ rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
- // Horizontal quad (two triangles)
- rawPositions.push(-size.x / 2, y - divisionWidth * size.y / 2, 0);
- rawPositions.push(size.x / 2, y - divisionWidth * size.y / 2, 0);
- rawPositions.push(size.x / 2, y + divisionWidth * size.y / 2, 0);
- rawPositions.push(-size.x / 2, y + divisionWidth * size.y / 2, 0);
- const baseIndex2 = (divisions + 1 + i) * 4;
- rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
- rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
- }
- return [rawPositions, rawIndices];
-}
\ No newline at end of file
+ // Horizontal quad (two triangles)
+ rawPositions.push(-size.x / 2, y - (divisionWidth * size.y) / 2, 0);
+ rawPositions.push(size.x / 2, y - (divisionWidth * size.y) / 2, 0);
+ rawPositions.push(size.x / 2, y + (divisionWidth * size.y) / 2, 0);
+ rawPositions.push(-size.x / 2, y + (divisionWidth * size.y) / 2, 0);
+ const baseIndex2 = (divisions + 1 + i) * 4;
+ rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
+ rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
+ }
+ return [rawPositions, rawIndices];
+}
diff --git a/frontend/misc/settings.ts b/frontend/misc/settings.ts
index 903b7d6..0c64cd8 100644
--- a/frontend/misc/settings.ts
+++ b/frontend/misc/settings.ts
@@ -85,7 +85,7 @@ export const settings = (async () => {
url = "dev+http://localhost:32323";
}
}
- settings.preload[i] = url;
+ settings.preload[i] = url ?? "";
}
// Auto-decompress the code and other playground settings
diff --git a/frontend/models/Model.vue b/frontend/models/Model.vue
index a115090..e230fdb 100644
--- a/frontend/models/Model.vue
+++ b/frontend/models/Model.vue
@@ -45,7 +45,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{ remove: [] }>()
-let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
+let modelName = props.meshes[0]?.getExtras()?.[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
// Count the number of faces, edges and vertices
let faceCount = ref(-1);
@@ -169,9 +169,9 @@ function onClipPlanesChange() {
new Plane(new Vector3(0, -1, 0), offsetY).applyMatrix4(rotSceneMatrix),
new Plane(new Vector3(0, 0, 1), -offsetZ).applyMatrix4(rotSceneMatrix),
];
- if (clipPlaneSwappedX.value) planes[0].negate();
- if (clipPlaneSwappedY.value) planes[1].negate();
- if (clipPlaneSwappedZ.value) planes[2].negate();
+ if (clipPlaneSwappedX.value) planes[0]?.negate();
+ if (clipPlaneSwappedY.value) planes[1]?.negate();
+ if (clipPlaneSwappedZ.value) planes[2]?.negate();
if (!enabledZ) planes.pop();
if (!enabledY) planes.splice(1, 1);
if (!enabledX) planes.shift();
@@ -575,4 +575,4 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
.mdi-triangle-outline { /* HACK: mdi is not fully imported, only required icons... */
background-image: url('data:image/svg+xml;charset=UTF-8,');
}
-
\ No newline at end of file
+
diff --git a/frontend/models/Models.vue b/frontend/models/Models.vue
index e1e5bdc..13f6ef1 100644
--- a/frontend/models/Models.vue
+++ b/frontend/models/Models.vue
@@ -17,7 +17,7 @@ function meshesList(sceneDocument: Document): Array> {
// Grouped by shared name
return sceneDocument.getRoot().listMeshes().reduce((acc, mesh) => {
let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed';
- let group = acc.find((group) => meshName(group[0]) === name);
+ let group = acc.find((group) => group[0] && meshName(group[0]) === name);
if (group) {
group.push(mesh);
} else {
@@ -43,9 +43,9 @@ defineExpose({findModel})
-
-
+
diff --git a/frontend/tools/Selection.vue b/frontend/tools/Selection.vue
index 3a22359..2cb17db 100644
--- a/frontend/tools/Selection.vue
+++ b/frontend/tools/Selection.vue
@@ -213,7 +213,7 @@ function toggleSelection() {
function toggleOpenNextSelection() {
openNextSelection.value = [
!openNextSelection.value[0],
- openNextSelection.value[0] ? openNextSelection.value[1] : selectionEnabled.value
+ openNextSelection.value[0] ? (openNextSelection.value[1] ?? false) : selectionEnabled.value
];
if (openNextSelection.value[0]) {
// Reuse selection code to identify the model
@@ -330,14 +330,20 @@ function updateBoundingBox() {
// Only draw one edge per axis, the 2nd closest one to the camera
for (let edgeI in edgesByAxis) {
let axisEdges = edgesByAxis[edgeI];
- let edge: Array = axisEdges[0];
+ if (!axisEdges || axisEdges.length === 0) continue;
+ let edge: Array = axisEdges[0] ?? [];
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
- edge = axisEdges[0];
+ if (!axisEdges || axisEdges.length === 0) break;
+ edge = axisEdges[0] ?? [];
let edgeDist = Infinity;
let cameraPos: Vector3 = props.viewer?.scene?.camera?.position ?? new Vector3();
for (let testEdge of axisEdges) {
- let from = new Vector3(...corners[testEdge[0]]);
- let to = new Vector3(...corners[testEdge[1]]);
+ if (!testEdge || testEdge.length < 2) continue;
+ let cornerA = corners[testEdge[0] ?? 0];
+ let cornerB = corners[testEdge[1] ?? 0];
+ if (!cornerA || !cornerB) continue;
+ let from = new Vector3(...cornerA);
+ let to = new Vector3(...cornerB);
let mid = from.clone().add(to).multiplyScalar(0.5);
let newDist = cameraPos.distanceTo(mid);
if (newDist < edgeDist) {
@@ -347,11 +353,16 @@ function updateBoundingBox() {
}
axisEdges = axisEdges.filter((e) => e !== edge);
}
- let from = new Vector3(...corners[edge[0]]);
- let to = new Vector3(...corners[edge[1]]);
+ if (!edge || edge.length < 2) continue;
+ let cornerA = corners[edge[0] ?? 0];
+ let cornerB = corners[edge[1] ?? 0];
+ if (!cornerA || !cornerB) continue;
+ let from = new Vector3(...cornerA);
+ let to = new Vector3(...cornerB);
let length = to.clone().sub(from).length();
if (length < 0.05) continue; // Skip very small edges (e.g. a single point)
- let color = [AxesColors.x, AxesColors.y, AxesColors.z][edgeI][1]; // Secondary colors
+ let colorArray = [AxesColors.x, AxesColors.y, AxesColors.z][parseInt(edgeI)];
+ let color = colorArray ? colorArray[1] : [255, 255, 255]; // Secondary colors
let lineCacheKey = JSON.stringify([from, to]);
let matchingLine = boundingBoxLines[lineCacheKey];
if (matchingLine) {
@@ -359,7 +370,7 @@ function updateBoundingBox() {
} else {
let newLineId = props.viewer?.addLine3D(from, to,
length.toFixed(1) + "mm", {
- "stroke": "rgb(" + color.join(',') + ")",
+ "stroke": "rgb(" + (color ?? [255, 255, 255]).join(',') + ")",
"stroke-width": "2"
});
if (newLineId) {
@@ -410,10 +421,17 @@ function updateDistances() {
}
// Add lines (if not already added)
- let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
- 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");
+ if (!selected.value[0] || !selected.value[1] || !props.viewer?.scene) return;
+ let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer.scene);
+ if (max[0] && max[1]) {
+ ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
+ }
+ if (center[0] && center[1]) {
+ ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
+ }
+ if (min[0] && min[1]) {
+ 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) {
@@ -504,4 +522,4 @@ window.addEventListener('keydown', (event) => {
top: -12px;
width: calc(100% - 48px);
}
-
\ No newline at end of file
+
diff --git a/frontend/tools/selection.ts b/frontend/tools/selection.ts
index 31f37a4..3c36442 100644
--- a/frontend/tools/selection.ts
+++ b/frontend/tools/selection.ts
@@ -1,163 +1,182 @@
// 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";
+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]
+ /** 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;
- }
+ 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 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 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 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]);
+ 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] ?? Infinity, x);
+ min[1] = Math.min(min[1] ?? Infinity, y);
+ min[2] = Math.min(min[2] ?? Infinity, z);
+ max[0] = Math.max(max[0] ?? -Infinity, x);
+ max[1] = Math.max(max[1] ?? -Infinity, y);
+ max[2] = Math.max(max[2] ?? -Infinity, 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;
+ 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) 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]];
- }
- }
+ let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
+ if (!hit.faceIndex) 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 ?? 0];
+ } 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;
+ }
+ 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]];
- }
+ 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;
+ }
+ 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;
+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]);
}
- return prevColor!;
+ 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]);
+ // 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()];
-}
\ No newline at end of file
+ // 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()];
+}
diff --git a/frontend/viewer/ModelViewerWrapper.vue b/frontend/viewer/ModelViewerWrapper.vue
index 008cc6b..ca95ea7 100644
--- a/frontend/viewer/ModelViewerWrapper.vue
+++ b/frontend/viewer/ModelViewerWrapper.vue
@@ -161,9 +161,9 @@ function addLine3D(p1: Vector3, p2: Vector3, centerText?: string, lineAttrs: { [
function removeLine3D(id: number): boolean {
if (!scene.value || !(id in lines.value)) return false;
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'}));
- lines.value[id].startHotspot.parentElement?.remove()
+ lines.value[id]?.startHotspot.parentElement?.remove()
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];
scene.value.queueRender() // Needed to update the hotspots
return true;
@@ -175,17 +175,17 @@ function onCameraChangeLine(lineId: number) {
if (!(lineId in lines.value) || !(elem.value)) return // Silently ignore (not updated yet)
// Update start and end 2D positions
let {x: xB, y: yB} = elem.value.getBoundingClientRect();
- let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect();
- lines.value[lineId].start2D = [x - xB, y - yB];
- let {x: x2, y: y2} = lines.value[lineId].endHotspot.getBoundingClientRect();
- lines.value[lineId].end2D = [x2 - xB, y2 - yB];
+ let {x, y} = lines.value[lineId]?.startHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
+ if (lines.value[lineId]) lines.value[lineId].start2D = [x - xB, y - yB];
+ let {x: x2, y: y2} = lines.value[lineId]?.endHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
+ if (lines.value[lineId]) lines.value[lineId].end2D = [x2 - xB, y2 - yB];
// Update the center text size if needed
- if (svg.value && lines.value[lineId].centerText && lines.value[lineId].centerTextSize[0] === 0) {
+ if (svg.value && lines.value[lineId]?.centerText && lines.value[lineId]?.centerTextSize[0] === 0) {
let text = svg.value.getElementsByClassName('line' + lineId + '_text')[0] as SVGTextElement | undefined;
if (text) {
let bbox = text.getBBox();
- lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
+ if (lines.value[lineId]) lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
}
}
}
diff --git a/frontend/viewer/lighting.ts b/frontend/viewer/lighting.ts
index 34c48f4..e48f53b 100644
--- a/frontend/viewer/lighting.ts
+++ b/frontend/viewer/lighting.ts
@@ -1,76 +1,96 @@
-import {ModelViewerElement} from '@google/model-viewer';
-import {$scene} from "@google/model-viewer/lib/model-viewer-base";
-import {settings} from "../misc/settings.ts";
+import { ModelViewerElement } from "@google/model-viewer";
+import { $scene } from "@google/model-viewer/lib/model-viewer-base";
+import { settings } from "../misc/settings.ts";
export let currentSceneRotation = 0; // radians, 0 is the default rotation
export async function setupLighting(modelViewer: ModelViewerElement) {
- modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
- // Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
- let lastX: number;
- let panning = false;
- let radiansPerPixel: number;
+ modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
+ // Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
+ let lastX: number;
+ let panning = false;
+ let radiansPerPixel: number;
- const startPan = () => {
- const orbit = modelViewer.getCameraOrbit();
- const {radius} = orbit;
- radiansPerPixel = -1 * radius / modelViewer.getBoundingClientRect().height;
- modelViewer.interactionPrompt = 'none';
- };
+ const startPan = () => {
+ const orbit = modelViewer.getCameraOrbit();
+ const { radius } = orbit;
+ radiansPerPixel = (-1 * radius) / modelViewer.getBoundingClientRect().height;
+ modelViewer.interactionPrompt = "none";
+ };
- const updatePan = (thisX: number) => {
- const delta = (thisX - lastX) * radiansPerPixel;
- lastX = thisX;
- currentSceneRotation += delta;
- const orbit = modelViewer.getCameraOrbit();
- orbit.theta += delta;
- modelViewer.cameraOrbit = orbit.toString();
- modelViewer.resetTurntableRotation(currentSceneRotation);
- modelViewer.jumpCameraToGoal();
- }
+ const updatePan = (thisX: number) => {
+ const delta = (thisX - lastX) * radiansPerPixel;
+ lastX = thisX;
+ currentSceneRotation += delta;
+ const orbit = modelViewer.getCameraOrbit();
+ orbit.theta += delta;
+ modelViewer.cameraOrbit = orbit.toString();
+ modelViewer.resetTurntableRotation(currentSceneRotation);
+ modelViewer.jumpCameraToGoal();
+ };
- modelViewer.addEventListener('mousedown', (event) => {
- panning = event.metaKey || event.shiftKey;
- if (!panning)
- return;
+ modelViewer.addEventListener(
+ "mousedown",
+ (event) => {
+ panning = event.metaKey || event.shiftKey;
+ if (!panning) return;
- lastX = event.clientX;
- startPan();
- event.stopPropagation();
- }, true);
+ lastX = event.clientX;
+ startPan();
+ event.stopPropagation();
+ },
+ true,
+ );
- modelViewer.addEventListener('touchstart', (event) => {
- const {targetTouches, touches} = event;
- panning = targetTouches.length === 2 && targetTouches.length === touches.length;
- if (!panning)
- return;
+ modelViewer.addEventListener(
+ "touchstart",
+ (event) => {
+ const { targetTouches, touches } = event;
+ panning = targetTouches.length === 2 && targetTouches.length === touches.length;
+ if (!panning) return;
- lastX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
- startPan();
- }, true);
+ lastX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
+ startPan();
+ },
+ true,
+ );
- document.addEventListener('mousemove', (event) => {
- if (!panning)
- return;
+ document.addEventListener(
+ "mousemove",
+ (event) => {
+ if (!panning) return;
- updatePan(event.clientX);
- event.stopPropagation();
- }, true);
+ updatePan(event.clientX);
+ event.stopPropagation();
+ },
+ true,
+ );
- modelViewer.addEventListener('touchmove', (event) => {
- if (!panning || event.targetTouches.length !== 2)
- return;
+ modelViewer.addEventListener(
+ "touchmove",
+ (event) => {
+ if (!panning || event.targetTouches.length !== 2) return;
- const {targetTouches} = event;
- const thisX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
- updatePan(thisX);
- }, true);
+ const { targetTouches } = event;
+ const thisX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
+ updatePan(thisX);
+ },
+ true,
+ );
- document.addEventListener('mouseup', (event) => {
- panning = false;
- }, true);
+ document.addEventListener(
+ "mouseup",
+ (event) => {
+ panning = false;
+ },
+ true,
+ );
- modelViewer.addEventListener('touchend', (event) => {
- panning = false;
- }, true);
-}
\ No newline at end of file
+ modelViewer.addEventListener(
+ "touchend",
+ (event) => {
+ panning = false;
+ },
+ true,
+ );
+}
diff --git a/package.json b/package.json
index 72f6354..14cd114 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"@types/three": "^0.179.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.0",
- "@vue/tsconfig": "^0.7.0",
+ "@vue/tsconfig": "^0.8.0",
"buffer": "^5.5.0||^6.0.0",
"commander": "^14.0.0",
"generate-license-file": "^4.0.0",
diff --git a/yarn.lock b/yarn.lock
index 10af0ae..5045405 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1269,10 +1269,10 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.20.tgz#8740b370738c8c7e29e02fa9051cfe6d20114cb4"
integrity sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==
-"@vue/tsconfig@^0.7.0":
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz#67044c847b7a137b8cbfd6b23104c36dbaf80d1d"
- integrity sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==
+"@vue/tsconfig@^0.8.0":
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.8.1.tgz#4732251fa58945024424385cf3be0b1708fad5fe"
+ integrity sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==
"@webgpu/types@*":
version "0.1.64"