From 632e7e93c6928cf510913b247ca6265d975709ee Mon Sep 17 00:00:00 2001 From: Yeicor <4929005+Yeicor@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:37:28 +0100 Subject: [PATCH] lots of performance improvements, bug fixes and some new features --- assets/licenses.txt | 51 ++++- frontend/App.vue | 4 +- frontend/misc/distances.ts | 23 +- frontend/misc/gltf.ts | 14 +- frontend/misc/helpers.ts | 55 +++-- frontend/misc/lines.ts | 55 +++++ frontend/misc/scene.ts | 8 +- frontend/misc/settings.ts | 1 + frontend/models/Model.vue | 129 +++++++++-- frontend/tools/OrientationGizmo.vue | 2 +- frontend/tools/Selection.vue | 84 ++++--- frontend/tools/Tools.vue | 10 +- frontend/tools/selection.ts | 151 +++++++++++++ frontend/viewer/ModelViewerWrapper.vue | 8 + package.json | 9 +- vite.config.ts | 1 + yacv_server/__init__.py | 2 +- yacv_server/cad.py | 25 ++- yacv_server/gltf.py | 299 +++++++++++++------------ yacv_server/logo.py | 9 +- yacv_server/tessellate.py | 23 +- yarn.lock | 43 ++-- 22 files changed, 710 insertions(+), 296 deletions(-) create mode 100644 frontend/misc/lines.ts create mode 100644 frontend/tools/selection.ts diff --git a/assets/licenses.txt b/assets/licenses.txt index d2e7628..db83252 100644 --- a/assets/licenses.txt +++ b/assets/licenses.txt @@ -211,11 +211,12 @@ Apache License ----------- -The following npm package may be included in this product: +The following npm packages may be included in this product: - source-map-js@1.0.2 + - source-map-js@1.2.0 -This package contains the following license and notice below: +These packages each contain the following license and notice below: Copyright (c) 2009-2011, Mozilla Foundation and contributors All rights reserved. @@ -1290,7 +1291,7 @@ third-party archives. The following npm package may be included in this product: - - typescript@5.4.2 + - typescript@5.4.3 This package contains the following license and notice below: @@ -1764,6 +1765,36 @@ SOFTWARE. ----------- +The following npm package may be included in this product: + + - three-mesh-bvh@0.7.3 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2018 Garrett Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + The following npm package may be included in this product: - napi-build-utils@1.0.2 @@ -2194,13 +2225,13 @@ THE SOFTWARE. The following npm package may be included in this product: - - three@0.160.1 + - three@0.162.0 This package contains the following license and notice below: The MIT License -Copyright © 2010-2023 three.js authors +Copyright © 2010-2024 three.js authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2408,7 +2439,7 @@ THE SOFTWARE. The following npm package may be included in this product: - - vuetify@3.5.8 + - vuetify@3.5.11 This package contains the following license and notice below: @@ -2690,9 +2721,9 @@ THE SOFTWARE. The following npm packages may be included in this product: - - @gltf-transform/core@3.10.0 - - @gltf-transform/extensions@3.10.0 - - @gltf-transform/functions@3.10.0 + - @gltf-transform/core@3.10.1 + - @gltf-transform/extensions@3.10.1 + - @gltf-transform/functions@3.10.1 These packages each contain the following license and notice below: @@ -2843,7 +2874,7 @@ THE SOFTWARE. The following npm package may be included in this product: - - postcss@8.4.35 + - postcss@8.4.38 This package contains the following license and notice below: diff --git a/frontend/App.vue b/frontend/App.vue index 144d8cd..e4debd3 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -47,9 +47,9 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) { let model = event.models[modelIndex]; try { if (!model.isRemove) { - doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast, isLast); + doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast); } else { - doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast); + doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast); } } catch (e) { console.error("Error loading model", model, e); diff --git a/frontend/misc/distances.ts b/frontend/misc/distances.ts index 60eef3b..8af2e69 100644 --- a/frontend/misc/distances.ts +++ b/frontend/misc/distances.ts @@ -1,16 +1,17 @@ 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 {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene'; +import type {SelectionInfo} from "../tools/selection"; -function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): { +function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): { center: Vector3, vertices: Array } { - obj.updateMatrixWorld(); - let pos: BufferAttribute | InterleavedBufferAttribute = obj.geometry.getAttribute('position'); - let ind: BufferAttribute | null = obj.geometry.index; - if (!ind) { + selInfo.object.updateMatrixWorld(); + 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; @@ -18,14 +19,14 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): { } let center = new Vector3(); let vertices = []; - for (let i = 0; i < ind.count; i++) { - let index = ind.array[i]; + 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(obj.localToWorld(vertex)); + vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex)); center.add(vertex); vertices.push(vertex); } - center = center.divideScalar(ind.count); + center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]); return {center, vertices}; } @@ -33,7 +34,7 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): { * 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: MObject3D, b: MObject3D, scene: ModelScene): { +export function distances(a: SelectionInfo, b: SelectionInfo, scene: ModelScene): { min: Array, center: Array, max: Array diff --git a/frontend/misc/gltf.ts b/frontend/misc/gltf.ts index b261b48..f5c32c5 100644 --- a/frontend/misc/gltf.ts +++ b/frontend/misc/gltf.ts @@ -1,4 +1,4 @@ -import {Document, Scene, type Transform, WebIO, Buffer} from "@gltf-transform/core"; +import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core"; import {unpartition} from "@gltf-transform/functions"; let io = new WebIO(); @@ -12,10 +12,16 @@ export let extrasNameValueHelpers = "__helpers"; * * Remember to call mergeFinalize after all models have been merged (slower required operations). */ -export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => {}): Promise { +export async function mergePartial(url: string, 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 fetch(url); + let buffer = await response.arrayBuffer(); + networkFinished(); + // Load the new document - let newDoc = await io.read(url); - networkFinished() + let newDoc = await io.readBinary(new Uint8Array(buffer)); // Remove any previous model with the same name await document.transform(dropByName(name)); diff --git a/frontend/misc/helpers.ts b/frontend/misc/helpers.ts index 6bdc034..bf18e49 100644 --- a/frontend/misc/helpers.ts +++ b/frontend/misc/helpers.ts @@ -1,3 +1,5 @@ +// 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" @@ -26,7 +28,7 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb if (rawColors) { colors = doc.createAccessor(name + 'Color') .setArray(new Float32Array(rawColors) as TypedArray) - .setType('VEC3') + .setType('VEC4') .setBuffer(buffer); } const material = doc.createMaterial(name + 'Material') @@ -39,6 +41,11 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb 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) @@ -48,21 +55,19 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb * 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], + 0, 0, 0, size.x, 0, 0, + 0, 0, 0, 0, size.y, 0, + 0, 0, 0, 0, 0, -size.z, ]; - let rawIndices = [0, 1]; let rawColors = [ - [...(AxesColors.x[0]), ...(AxesColors.x[1])], - [...(AxesColors.y[0]), ...(AxesColors.y[1])], - [...(AxesColors.z[0]), ...(AxesColors.z[1])], - ].map(g => g.map(x => x / 255.0)); - buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes'); - buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes'); - buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes'); - buildSimpleGltf(doc, [0, 0, 0], [0], null, transform, '__helper_axes', WebGL2RenderingContext.POINTS); + ...(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); + buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes'); // Axes at (0,0,0)! + buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes', WebGL2RenderingContext.POINTS); } /** @@ -71,8 +76,10 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) { * The grid is built as a box of triangles (representing lines) looking to the inside of the box. * 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 = new Matrix4(), divisions = 10) { +export async 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)); @@ -82,13 +89,25 @@ export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4 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 = baseTransform.clone().multiply(translation).multiply(rotation); - newGridPlane(doc, size2, transform, divisions); + 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); } -export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.002) { +export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] { const rawPositions = []; const rawIndices = []; // Build the grid as triangles @@ -114,5 +133,5 @@ export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2); rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3); } - buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES); + return [rawPositions, rawIndices]; } \ No newline at end of file diff --git a/frontend/misc/lines.ts b/frontend/misc/lines.ts new file mode 100644 index 0000000..140cab5 --- /dev/null +++ b/frontend/misc/lines.ts @@ -0,0 +1,55 @@ +import {BufferGeometry} from 'three/src/core/BufferGeometry.js'; +import {Vector2} from 'three/src/math/Vector2.js'; + +// The following imports must be done dynamically to be able to import three.js separately (smaller bundle sizee) +// import {LineSegments2} from "three/examples/jsm/lines/LineSegments2.js"; +// import {LineMaterial} from "three/examples/jsm/lines/LineMaterial.js"; +// import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; +const LineSegments2Import = import('three/examples/jsm/lines/LineSegments2.js'); +const LineMaterialImport = import('three/examples/jsm/lines/LineMaterial.js'); +const LineSegmentsGeometryImport = import('three/examples/jsm/lines/LineSegmentsGeometry.js'); + +export async function toLineSegments(bufferGeometry: BufferGeometry) { + const LineSegments2 = (await LineSegments2Import).LineSegments2; + const LineMaterial = (await LineMaterialImport).LineMaterial; + return new LineSegments2(await toLineSegmentsGeometry(bufferGeometry), new LineMaterial({ + color: 0xffffffff, + vertexColors: true, + linewidth: 0.1, // mm + worldUnits: true, + resolution: new Vector2(1, 1), // Update resolution on resize!!! + })); +} + +async function toLineSegmentsGeometry(bufferGeometry: BufferGeometry) { + const LineSegmentsGeometry = (await LineSegmentsGeometryImport).LineSegmentsGeometry; + const lg = new LineSegmentsGeometry(); + + const position = bufferGeometry.getAttribute('position'); + const indexAttribute = bufferGeometry.index!!; + const positions = []; + for (let index = 0; index != indexAttribute.count; ++index) { + const i = indexAttribute.getX(index); + const x = position.getX(i); + const y = position.getY(i); + const z = position.getZ(i); + positions.push(x, y, z); + } + lg.setPositions(positions); + + const colors = []; + const color = bufferGeometry.getAttribute('color'); + if (color) { + for (let index = 0; index != indexAttribute.count; ++index) { + const i = indexAttribute.getX(index); + const r = color.getX(i); + const g = color.getY(i); + const b = color.getZ(i); + colors.push(r, g, b); + } + lg.setColors(colors); + } + + lg.userData = bufferGeometry.userData; + return lg; +} \ No newline at end of file diff --git a/frontend/misc/scene.ts b/frontend/misc/scene.ts index 64e6722..6b986e1 100644 --- a/frontend/misc/scene.ts +++ b/frontend/misc/scene.ts @@ -9,13 +9,15 @@ import {Matrix4} from "three/src/math/Matrix4.js" /** This class helps manage SceneManagerData. All methods are static to support reactivity... */ export class SceneMgr { /** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */ - static async loadModel(sceneUrl: Ref, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true, networkFinished: () => void = () => {}): Promise { + static async loadModel(sceneUrl: Ref, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise { let loadStart = performance.now(); + let loadNetworkEnd: number; // Start merging into the current document, replacing or adding as needed - document = await mergePartial(url, name, document, networkFinished); + document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now()); - console.log("Model", name, "loaded in", performance.now() - loadStart, "ms"); + console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after", + loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)"); if (updateHelpers) { // Reload the helpers to fit the new model diff --git a/frontend/misc/settings.ts b/frontend/misc/settings.ts index 4d67543..2ad21fb 100644 --- a/frontend/misc/settings.ts +++ b/frontend/misc/settings.ts @@ -12,6 +12,7 @@ export const settings = { // Websocket URLs automatically listen for new models from the python backend "dev+http://127.0.0.1:32323/" ], + loadHelpers: true, displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ monitorEveryMs: 100, monitorOpenTimeoutMs: 1000, diff --git a/frontend/models/Model.vue b/frontend/models/Model.vue index 1ab780d..0982de3 100644 --- a/frontend/models/Model.vue +++ b/frontend/models/Model.vue @@ -25,13 +25,14 @@ import { mdiVectorRectangle } from '@mdi/js' import SvgIcon from '@jamescoyle/vue-icon'; -import {SceneMgr} from "../misc/scene"; import {BackSide, FrontSide} from "three/src/constants.js"; import {Box3} from "three/src/math/Box3.js"; import {Color} from "three/src/math/Color.js"; import {Plane} from "three/src/math/Plane.js"; import {Vector3} from "three/src/math/Vector3.js"; +import {Vector2} from "three/src/math/Vector2.js"; import type {MObject3D} from "../tools/Selection.vue"; +import {toLineSegments} from "../misc/lines.js"; const props = defineProps<{ meshes: Array, @@ -44,6 +45,8 @@ let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah // Reactive properties const enabledFeatures = defineModel>("enabledFeatures", {default: [0, 1, 2]}); const opacity = defineModel("opacity", {default: 1}); +const wireframe = ref(false); +// Clipping planes are handled in y-up space (swapped on interface, Z inverted later) const clipPlaneX = ref(1); const clipPlaneSwappedX = ref(false); const clipPlaneY = ref(1); @@ -52,9 +55,18 @@ const clipPlaneZ = ref(1); const clipPlaneSwappedZ = ref(false); // Count the number of faces, edges and vertices -let faceCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length).reduce((a, b) => a + b, 0) -let edgeCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]).length).reduce((a, b) => a + b, 0) -let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0) +let faceCount = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES)) + .map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1) + .reduce((a, b) => a + b, 0) +let edgeCount = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES])) + .map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0) + .reduce((a, b) => a + b, 0) +let vertexCount = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS)) + .map(p => (p.getAttribute("POSITION")?.getCount() ?? 0)) + .reduce((a, b) => a + b, 0) // Set initial defaults for the enabled features if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0) @@ -73,7 +85,7 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array) { sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh' - let childIsEdge = child.type == 'Line' || child.type == 'LineSegments' + let childIsEdge = child.type == 'Line' || child.type == 'LineSegments' || child.type == 'LineSegments2' let childIsVertex = child.type == 'Points' if (childIsFace || childIsEdge || childIsVertex) { let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1); @@ -111,6 +123,27 @@ function onOpacityChange(newOpacity: number) { watch(opacity, onOpacityChange); +function onWireframeChange(newWireframe: boolean) { + let scene = props.viewer?.scene; + let sceneModel = (scene as any)?._model; + if (!scene || !sceneModel) return; + // Iterate all primitives of the mesh and set their wireframe based on the enabled features + // Use the scene graph instead of the document to avoid reloading the same model, at the cost + // of not actually removing the primitives from the scene graph + // console.log('Wireframe may have changed', newWireframe) + sceneModel.traverse((child: MObject3D) => { + if (child.userData[extrasNameKey] === modelName) { + if (child.material && child.material.wireframe !== newWireframe) { + child.material.wireframe = newWireframe; + child.material.needsUpdate = true; + } + } + }); + scene.queueRender() +} + +watch(wireframe, onWireframeChange); + let {sceneDocument} = inject<{ sceneDocument: ShallowRef }>('sceneDocument')!!; function onClipPlanesChange() { @@ -125,22 +158,25 @@ function onClipPlanesChange() { if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) { // Global value for all models, once set it cannot be unset (unknown for other models...) props.viewer.renderer.threeRenderer.localClippingEnabled = true; - // Due to model-viewer's camera manipulation, the bounding box needs to be transformed - let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value); - if (!boundingBox) return; // No models. Should not happen. - bbox = boundingBox.translate(scene.getTarget()); + // Get the bounding box containing all features of this model + bbox = new Box3(); + sceneModel.traverse((child: MObject3D) => { + if (child.userData[extrasNameKey] === modelName) { + bbox.expandByObject(child); + } + }); } sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { if (child.material) { - if (bbox) { + if (bbox?.isEmpty() == false) { let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x); - let offsetY = bbox.min.z + clipPlaneY.value * (bbox.max.z - bbox.min.z); - let offsetZ = bbox.min.y + clipPlaneZ.value * (bbox.max.y - bbox.min.y); + let offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y); + let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z); let planes = [ new Plane(new Vector3(-1, 0, 0), offsetX), - new Plane(new Vector3(0, 0, 1), offsetY), - new Plane(new Vector3(0, -1, 0), offsetZ), + new Plane(new Vector3(0, -1, 0), offsetY), + new Plane(new Vector3(0, 0, 1), -offsetZ), ]; if (clipPlaneSwappedX.value) planes[0].negate(); if (clipPlaneSwappedY.value) planes[1].negate(); @@ -177,9 +213,16 @@ function onModelLoad() { // Use the scene graph instead of the document to avoid reloading the same model, at the cost // of not actually removing the primitives from the scene graph let childrenToAdd: Array = []; + let linesToImprove: Array = []; sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { if (child.type == 'Mesh' || child.type == 'SkinnedMesh') { + // Compute a BVH for faster raycasting (MUCH faster selection) + // @ts-ignore + child.geometry?.computeBoundsTree({indirect: true}); // indirect to avoid changing index order + // TODO: Accelerated raycast for lines and points (https://github.com/gkjohnson/three-mesh-bvh/issues/243) + // TODO: ParallelMeshBVHWorker + // We could implement cutting planes using the stencil buffer: // https://threejs.org/examples/?q=clipping#webgl_clipping_stencil // But this is buggy for lots of models, so instead we just draw @@ -194,29 +237,55 @@ function onModelLoad() { backChild.material.side = BackSide; backChild.material.color = new Color(0.25, 0.25, 0.25) child.userData.backChild = backChild; + backChild.userData.noHit = true; childrenToAdd.push(backChild as MObject3D); } } - // if (child.type == 'Line' || child.type == 'LineSegments') { - // child.material.linewidth = 3; // Not supported in WebGL2 - // If wide lines are really needed, we need https://threejs.org/examples/?q=line#webgl_lines_fat - // } + if (child.type == 'Line' || child.type == 'LineSegments') { + // child.material.linewidth = 3; // Not supported in WebGL2 + // Swap geometry with LineGeometry to support widths + // https://threejs.org/examples/?q=line#webgl_lines_fat + linesToImprove.push(child); + } if (child.type == 'Points') { - (child.material as any).size = 5; + (child.material as any).size = 7; child.material.needsUpdate = true; } } }); childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child)); - scene.queueRender() + linesToImprove.forEach(async (line: MObject3D) => { + let line2 = await toLineSegments(line.geometry); + // Update resolution on resize + props.viewer!!.onElemReady((elem) => { + let l = () => { + line2.material.resolution.set(elem.clientWidth, elem.clientHeight); + line2.material.needsUpdate = true; + }; + elem.addEventListener('resize', l); // TODO: Remove listener when line is replaced + l(); + }); + line2.computeLineDistances(); + line2.userData = Object.assign({}, line.userData); + line.parent!.add(line2); + line.children.forEach((o) => line2.add(o)); + line.visible = false; + line.userData.niceLine = line2; + // line.parent!.remove(line); // Keep it for better raycast and selection! + line2.userData.noHit = true; + }); // Furthermore... // Enabled features may have been reset after a reload onEnabledFeaturesChange(enabledFeatures.value) // Opacity may have been reset after a reload onOpacityChange(opacity.value) + // Wireframe may have been reset after a reload + onWireframeChange(wireframe.value) // Clip planes may have been reset after a reload onClipPlanesChange() + + scene.queueRender() } // props.viewer.elem may not yet be available, so we need to wait for it @@ -253,6 +322,10 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad)) Change opacity + @@ -271,7 +344,7 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad)) - +