mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
lots of performance improvements, bug fixes and some new features
This commit is contained in:
@@ -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.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
|
Copyright (c) 2009-2011, Mozilla Foundation and contributors
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
@@ -1290,7 +1291,7 @@ third-party archives.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
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:
|
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:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- napi-build-utils@1.0.2
|
- napi-build-utils@1.0.2
|
||||||
@@ -2194,13 +2225,13 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
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:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
The MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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:
|
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:
|
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:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @gltf-transform/core@3.10.0
|
- @gltf-transform/core@3.10.1
|
||||||
- @gltf-transform/extensions@3.10.0
|
- @gltf-transform/extensions@3.10.1
|
||||||
- @gltf-transform/functions@3.10.0
|
- @gltf-transform/functions@3.10.1
|
||||||
|
|
||||||
These packages each contain the following license and notice below:
|
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:
|
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:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
|||||||
let model = event.models[modelIndex];
|
let model = event.models[modelIndex];
|
||||||
try {
|
try {
|
||||||
if (!model.isRemove) {
|
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 {
|
} else {
|
||||||
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast);
|
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading model", model, e);
|
console.error("Error loading model", model, e);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
|
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
|
||||||
import type {MObject3D} from "../tools/Selection.vue";
|
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,
|
center: Vector3,
|
||||||
vertices: Array<Vector3>
|
vertices: Array<Vector3>
|
||||||
} {
|
} {
|
||||||
obj.updateMatrixWorld();
|
selInfo.object.updateMatrixWorld();
|
||||||
let pos: BufferAttribute | InterleavedBufferAttribute = obj.geometry.getAttribute('position');
|
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
||||||
let ind: BufferAttribute | null = obj.geometry.index;
|
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
||||||
if (!ind) {
|
if (ind === null) {
|
||||||
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
|
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
|
||||||
for (let i = 0; i < pos.count; i++) {
|
for (let i = 0; i < pos.count; i++) {
|
||||||
ind.array[i] = i;
|
ind.array[i] = i;
|
||||||
@@ -18,14 +19,14 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
|
|||||||
}
|
}
|
||||||
let center = new Vector3();
|
let center = new Vector3();
|
||||||
let vertices = [];
|
let vertices = [];
|
||||||
for (let i = 0; i < ind.count; i++) {
|
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
|
||||||
let index = ind.array[i];
|
let index = ind.getX(i)
|
||||||
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
|
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);
|
center.add(vertex);
|
||||||
vertices.push(vertex);
|
vertices.push(vertex);
|
||||||
}
|
}
|
||||||
center = center.divideScalar(ind.count);
|
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
|
||||||
return {center, vertices};
|
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.
|
* 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.
|
* 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<Vector3>,
|
min: Array<Vector3>,
|
||||||
center: Array<Vector3>,
|
center: Array<Vector3>,
|
||||||
max: Array<Vector3>
|
max: Array<Vector3>
|
||||||
|
|||||||
@@ -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";
|
import {unpartition} from "@gltf-transform/functions";
|
||||||
|
|
||||||
let io = new WebIO();
|
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).
|
* 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<Document> {
|
export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => {
|
||||||
|
}): Promise<Document> {
|
||||||
|
// 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
|
// Load the new document
|
||||||
let newDoc = await io.read(url);
|
let newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||||
networkFinished()
|
|
||||||
|
|
||||||
// Remove any previous model with the same name
|
// Remove any previous model with the same name
|
||||||
await document.transform(dropByName(name));
|
await document.transform(dropByName(name));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
|
||||||
|
|
||||||
import {Document, type TypedArray} from '@gltf-transform/core'
|
import {Document, type TypedArray} from '@gltf-transform/core'
|
||||||
import {Vector2} from "three/src/math/Vector2.js"
|
import {Vector2} from "three/src/math/Vector2.js"
|
||||||
import {Vector3} from "three/src/math/Vector3.js"
|
import {Vector3} from "three/src/math/Vector3.js"
|
||||||
@@ -26,7 +28,7 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
|||||||
if (rawColors) {
|
if (rawColors) {
|
||||||
colors = doc.createAccessor(name + 'Color')
|
colors = doc.createAccessor(name + 'Color')
|
||||||
.setArray(new Float32Array(rawColors) as TypedArray)
|
.setArray(new Float32Array(rawColors) as TypedArray)
|
||||||
.setType('VEC3')
|
.setType('VEC4')
|
||||||
.setBuffer(buffer);
|
.setBuffer(buffer);
|
||||||
}
|
}
|
||||||
const material = doc.createMaterial(name + 'Material')
|
const material = doc.createMaterial(name + 'Material')
|
||||||
@@ -39,6 +41,11 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
|||||||
if (rawColors) {
|
if (rawColors) {
|
||||||
geometry.setAttribute('COLOR_0', colors)
|
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 mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
|
||||||
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
|
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
|
||||||
scene.addChild(node)
|
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.
|
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
|
||||||
*/
|
*/
|
||||||
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||||
|
let rawIndices = [0, 1, 2, 3, 4, 5];
|
||||||
let rawPositions = [
|
let rawPositions = [
|
||||||
[0, 0, 0, size.x, 0, 0],
|
0, 0, 0, size.x, 0, 0,
|
||||||
[0, 0, 0, 0, size.y, 0],
|
0, 0, 0, 0, size.y, 0,
|
||||||
[0, 0, 0, 0, 0, -size.z],
|
0, 0, 0, 0, 0, -size.z,
|
||||||
];
|
];
|
||||||
let rawIndices = [0, 1];
|
|
||||||
let rawColors = [
|
let rawColors = [
|
||||||
[...(AxesColors.x[0]), ...(AxesColors.x[1])],
|
...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
|
||||||
[...(AxesColors.y[0]), ...(AxesColors.y[1])],
|
...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
|
||||||
[...(AxesColors.z[0]), ...(AxesColors.z[1])],
|
...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
|
||||||
].map(g => g.map(x => x / 255.0));
|
].map(x => x / 255.0);
|
||||||
buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes');
|
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes'); // Axes at (0,0,0)!
|
||||||
buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes');
|
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes', WebGL2RenderingContext.POINTS);
|
||||||
buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes');
|
|
||||||
buildSimpleGltf(doc, [0, 0, 0], [0], null, 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.
|
* 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.
|
* 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
|
// 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 axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
|
||||||
for (let positive of [1, -1]) {
|
for (let positive of [1, -1]) {
|
||||||
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
|
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.x) size2.set(size.z, size.y);
|
||||||
if (axis.y) size2.set(size.x, size.z);
|
if (axis.y) size2.set(size.x, size.z);
|
||||||
if (axis.z) size2.set(size.x, size.y);
|
if (axis.z) size2.set(size.x, size.y);
|
||||||
let transform = baseTransform.clone().multiply(translation).multiply(rotation);
|
let transform = new Matrix4().multiply(translation).multiply(rotation);
|
||||||
newGridPlane(doc, size2, transform, divisions);
|
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 rawPositions = [];
|
||||||
const rawIndices = [];
|
const rawIndices = [];
|
||||||
// Build the grid as triangles
|
// 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 + 1, baseIndex2 + 2);
|
||||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||||
}
|
}
|
||||||
buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
|
return [rawPositions, rawIndices];
|
||||||
}
|
}
|
||||||
55
frontend/misc/lines.ts
Normal file
55
frontend/misc/lines.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,13 +9,15 @@ import {Matrix4} from "three/src/math/Matrix4.js"
|
|||||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||||
export class SceneMgr {
|
export class SceneMgr {
|
||||||
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
|
/** 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<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true, networkFinished: () => void = () => {}): Promise<Document> {
|
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
||||||
let loadStart = performance.now();
|
let loadStart = performance.now();
|
||||||
|
let loadNetworkEnd: number;
|
||||||
|
|
||||||
// Start merging into the current document, replacing or adding as needed
|
// 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) {
|
if (updateHelpers) {
|
||||||
// Reload the helpers to fit the new model
|
// Reload the helpers to fit the new model
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const settings = {
|
|||||||
// Websocket URLs automatically listen for new models from the python backend
|
// Websocket URLs automatically listen for new models from the python backend
|
||||||
"dev+http://127.0.0.1:32323/"
|
"dev+http://127.0.0.1:32323/"
|
||||||
],
|
],
|
||||||
|
loadHelpers: true,
|
||||||
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
|
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
|
||||||
monitorEveryMs: 100,
|
monitorEveryMs: 100,
|
||||||
monitorOpenTimeoutMs: 1000,
|
monitorOpenTimeoutMs: 1000,
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ import {
|
|||||||
mdiVectorRectangle
|
mdiVectorRectangle
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import SvgIcon from '@jamescoyle/vue-icon';
|
import SvgIcon from '@jamescoyle/vue-icon';
|
||||||
import {SceneMgr} from "../misc/scene";
|
|
||||||
import {BackSide, FrontSide} from "three/src/constants.js";
|
import {BackSide, FrontSide} from "three/src/constants.js";
|
||||||
import {Box3} from "three/src/math/Box3.js";
|
import {Box3} from "three/src/math/Box3.js";
|
||||||
import {Color} from "three/src/math/Color.js";
|
import {Color} from "three/src/math/Color.js";
|
||||||
import {Plane} from "three/src/math/Plane.js";
|
import {Plane} from "three/src/math/Plane.js";
|
||||||
import {Vector3} from "three/src/math/Vector3.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 type {MObject3D} from "../tools/Selection.vue";
|
||||||
|
import {toLineSegments} from "../misc/lines.js";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
meshes: Array<Mesh>,
|
meshes: Array<Mesh>,
|
||||||
@@ -44,6 +45,8 @@ let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah
|
|||||||
// Reactive properties
|
// Reactive properties
|
||||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||||
const opacity = defineModel<number>("opacity", {default: 1});
|
const opacity = defineModel<number>("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 clipPlaneX = ref(1);
|
||||||
const clipPlaneSwappedX = ref(false);
|
const clipPlaneSwappedX = ref(false);
|
||||||
const clipPlaneY = ref(1);
|
const clipPlaneY = ref(1);
|
||||||
@@ -52,9 +55,18 @@ const clipPlaneZ = ref(1);
|
|||||||
const clipPlaneSwappedZ = ref(false);
|
const clipPlaneSwappedZ = ref(false);
|
||||||
|
|
||||||
// Count the number of faces, edges and vertices
|
// 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 faceCount = props.meshes
|
||||||
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)
|
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
||||||
let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0)
|
.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
|
// Set initial defaults for the enabled features
|
||||||
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||||
@@ -73,7 +85,7 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
|||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
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'
|
let childIsVertex = child.type == 'Points'
|
||||||
if (childIsFace || childIsEdge || childIsVertex) {
|
if (childIsFace || childIsEdge || childIsVertex) {
|
||||||
let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1);
|
let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1);
|
||||||
@@ -111,6 +123,27 @@ function onOpacityChange(newOpacity: number) {
|
|||||||
|
|
||||||
watch(opacity, onOpacityChange);
|
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<Document> }>('sceneDocument')!!;
|
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
|
||||||
|
|
||||||
function onClipPlanesChange() {
|
function onClipPlanesChange() {
|
||||||
@@ -125,22 +158,25 @@ function onClipPlanesChange() {
|
|||||||
if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) {
|
if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) {
|
||||||
// Global value for all models, once set it cannot be unset (unknown for other models...)
|
// Global value for all models, once set it cannot be unset (unknown for other models...)
|
||||||
props.viewer.renderer.threeRenderer.localClippingEnabled = true;
|
props.viewer.renderer.threeRenderer.localClippingEnabled = true;
|
||||||
// Due to model-viewer's camera manipulation, the bounding box needs to be transformed
|
// Get the bounding box containing all features of this model
|
||||||
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
|
bbox = new Box3();
|
||||||
if (!boundingBox) return; // No models. Should not happen.
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
bbox = boundingBox.translate(scene.getTarget());
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
|
bbox.expandByObject(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.material) {
|
if (child.material) {
|
||||||
if (bbox) {
|
if (bbox?.isEmpty() == false) {
|
||||||
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x);
|
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 offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y);
|
||||||
let offsetZ = bbox.min.y + clipPlaneZ.value * (bbox.max.y - bbox.min.y);
|
let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z);
|
||||||
let planes = [
|
let planes = [
|
||||||
new Plane(new Vector3(-1, 0, 0), offsetX),
|
new Plane(new Vector3(-1, 0, 0), offsetX),
|
||||||
new Plane(new Vector3(0, 0, 1), offsetY),
|
new Plane(new Vector3(0, -1, 0), offsetY),
|
||||||
new Plane(new Vector3(0, -1, 0), offsetZ),
|
new Plane(new Vector3(0, 0, 1), -offsetZ),
|
||||||
];
|
];
|
||||||
if (clipPlaneSwappedX.value) planes[0].negate();
|
if (clipPlaneSwappedX.value) planes[0].negate();
|
||||||
if (clipPlaneSwappedY.value) planes[1].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
|
// 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
|
// of not actually removing the primitives from the scene graph
|
||||||
let childrenToAdd: Array<MObject3D> = [];
|
let childrenToAdd: Array<MObject3D> = [];
|
||||||
|
let linesToImprove: Array<MObject3D> = [];
|
||||||
sceneModel.traverse((child: MObject3D) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
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:
|
// We could implement cutting planes using the stencil buffer:
|
||||||
// https://threejs.org/examples/?q=clipping#webgl_clipping_stencil
|
// https://threejs.org/examples/?q=clipping#webgl_clipping_stencil
|
||||||
// But this is buggy for lots of models, so instead we just draw
|
// 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.side = BackSide;
|
||||||
backChild.material.color = new Color(0.25, 0.25, 0.25)
|
backChild.material.color = new Color(0.25, 0.25, 0.25)
|
||||||
child.userData.backChild = backChild;
|
child.userData.backChild = backChild;
|
||||||
|
backChild.userData.noHit = true;
|
||||||
childrenToAdd.push(backChild as MObject3D);
|
childrenToAdd.push(backChild as MObject3D);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (child.type == 'Line' || child.type == 'LineSegments') {
|
if (child.type == 'Line' || child.type == 'LineSegments') {
|
||||||
// child.material.linewidth = 3; // Not supported in WebGL2
|
// 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
|
// Swap geometry with LineGeometry to support widths
|
||||||
// }
|
// https://threejs.org/examples/?q=line#webgl_lines_fat
|
||||||
|
linesToImprove.push(child);
|
||||||
|
}
|
||||||
if (child.type == 'Points') {
|
if (child.type == 'Points') {
|
||||||
(child.material as any).size = 5;
|
(child.material as any).size = 7;
|
||||||
child.material.needsUpdate = true;
|
child.material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child));
|
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...
|
// Furthermore...
|
||||||
// Enabled features may have been reset after a reload
|
// Enabled features may have been reset after a reload
|
||||||
onEnabledFeaturesChange(enabledFeatures.value)
|
onEnabledFeaturesChange(enabledFeatures.value)
|
||||||
// Opacity may have been reset after a reload
|
// Opacity may have been reset after a reload
|
||||||
onOpacityChange(opacity.value)
|
onOpacityChange(opacity.value)
|
||||||
|
// Wireframe may have been reset after a reload
|
||||||
|
onWireframeChange(wireframe.value)
|
||||||
// Clip planes may have been reset after a reload
|
// Clip planes may have been reset after a reload
|
||||||
onClipPlanesChange()
|
onClipPlanesChange()
|
||||||
|
|
||||||
|
scene.queueRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
// props.viewer.elem may not yet be available, so we need to wait for it
|
// 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))
|
|||||||
<v-tooltip activator="parent">Change opacity</v-tooltip>
|
<v-tooltip activator="parent">Change opacity</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCircleOpacity"></svg-icon>
|
<svg-icon type="mdi" :path="mdiCircleOpacity"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-tooltip activator="parent">Wireframe</v-tooltip>
|
||||||
|
<v-checkbox-btn trueIcon="mdi-triangle-outline" falseIcon="mdi-triangle" v-model="wireframe"></v-checkbox-btn>
|
||||||
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-slider v-model="clipPlaneX" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneX" hide-details min="0" max="1">
|
||||||
@@ -271,7 +344,7 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
|||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-slider v-model="clipPlaneY" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Clip plane Y</v-tooltip>
|
<v-tooltip activator="parent">Clip plane Y</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCube" :rotate="-120"></svg-icon>
|
<svg-icon type="mdi" :path="mdiCube" :rotate="-120"></svg-icon>
|
||||||
@@ -280,14 +353,14 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
|||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
|
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
||||||
v-model="clipPlaneSwappedY">
|
v-model="clipPlaneSwappedZ">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
|
<v-slider v-model="clipPlaneY" hide-details min="0" max="1">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Clip plane Z</v-tooltip>
|
<v-tooltip activator="parent">Clip plane Z</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCube"></svg-icon>
|
<svg-icon type="mdi" :path="mdiCube"></svg-icon>
|
||||||
@@ -296,7 +369,7 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
|||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
|
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
|
||||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
||||||
v-model="clipPlaneSwappedZ">
|
v-model="clipPlaneSwappedY">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
||||||
</template>
|
</template>
|
||||||
@@ -359,4 +432,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
|||||||
.mdi-checkbox-marked-outline { /* HACK: mdi is not fully imported, only required icons... */
|
.mdi-checkbox-marked-outline { /* HACK: mdi is not fully imported, only required icons... */
|
||||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"/></svg>');
|
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"/></svg>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdi-triangle { /* HACK: mdi is not fully imported, only required icons... */
|
||||||
|
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M1 21h22L12 2"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-triangle-outline { /* HACK: mdi is not fully imported, only required icons... */
|
||||||
|
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 2L1 21h22M12 6l7.53 13H4.47"/></svg>');
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -8,7 +8,7 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
|||||||
import {Vector3} from "three/src/math/Vector3.js";
|
import {Vector3} from "three/src/math/Vector3.js";
|
||||||
import {Matrix4} from "three/src/math/Matrix4.js";
|
import {Matrix4} from "three/src/math/Matrix4.js";
|
||||||
|
|
||||||
globalThis.THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
||||||
|
|
||||||
const OrientationGizmo = OrientationGizmoRaw.default;
|
const OrientationGizmo = OrientationGizmoRaw.default;
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,28 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
|||||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
|
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
|
||||||
import type {Intersection, Material, Mesh, Object3D} from "three";
|
import type {Intersection, Material, Mesh, Object3D} from "three";
|
||||||
import {Box3, Matrix4, Raycaster, Vector3} from "three";
|
import {Box3, Color, Raycaster, Vector3} from "three";
|
||||||
import type ModelViewerWrapperT from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapperT from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {extrasNameKey} from "../misc/gltf";
|
import {extrasNameKey} from "../misc/gltf";
|
||||||
import {SceneMgr} from "../misc/scene";
|
import {SceneMgr} from "../misc/scene";
|
||||||
import {Document} from "@gltf-transform/core";
|
import {Document} from "@gltf-transform/core";
|
||||||
import {AxesColors} from "../misc/helpers";
|
import {AxesColors} from "../misc/helpers";
|
||||||
import {distances} from "../misc/distances";
|
import {distances} from "../misc/distances";
|
||||||
|
import {highlight, highlightUndo, hitToSelectionInfo, type SelectionInfo} from "./selection";
|
||||||
|
|
||||||
export type MObject3D = Mesh & {
|
export type MObject3D = Mesh & {
|
||||||
userData: { noHit?: boolean },
|
userData: { noHit?: boolean },
|
||||||
material: Material & { color: { r: number, g: number, b: number }, __prevBaseColorFactor?: [number, number, number] }
|
material: Material & {
|
||||||
|
color: Color,
|
||||||
|
wireframe?: boolean
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
|
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
|
||||||
let emit = defineEmits<{ findModel: [string] }>();
|
let emit = defineEmits<{ findModel: [string] }>();
|
||||||
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
|
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
|
||||||
let selectionEnabled = ref(false);
|
let selectionEnabled = ref(false);
|
||||||
let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
|
let selected = defineModel<Array<SelectionInfo>>({default: []});
|
||||||
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||||
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
||||||
let showDistances = ref<Boolean>(true);
|
let showDistances = ref<Boolean>(true);
|
||||||
@@ -92,8 +96,27 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
// let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
|
// let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
|
||||||
// setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
|
// setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
|
||||||
|
|
||||||
// Find all hit objects and select the wanted one based on the filter
|
// Find all hit objects and raycast the wanted ones based on the filter
|
||||||
const hits = raycaster.intersectObject(scene, true);
|
let objects: Array<any> = [];
|
||||||
|
scene.traverse((obj) => {
|
||||||
|
const kind = obj.type
|
||||||
|
let isFace = kind === 'Mesh' || kind === 'SkinnedMesh';
|
||||||
|
let isEdge = kind === 'Line' || kind === 'LineSegments';
|
||||||
|
let isVertex = kind === 'Points';
|
||||||
|
if (obj.userData.noHit !== true &&
|
||||||
|
((selectFilter.value === 'Any (S)' && (isFace || isEdge || isVertex)) ||
|
||||||
|
(selectFilter.value === '(F)aces' && isFace) ||
|
||||||
|
(selectFilter.value === '(E)dges' && isEdge) ||
|
||||||
|
(selectFilter.value === '(V)ertices' && isVertex))) {
|
||||||
|
objects.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//console.log("Raycasting objects", objects)
|
||||||
|
|
||||||
|
// Run the raycaster on the selected objects only searching for the first hit
|
||||||
|
// @ts-ignore
|
||||||
|
raycaster.firstHitOnly = true;
|
||||||
|
const hits = raycaster.intersectObjects(objects, false);
|
||||||
let hit = hits
|
let hit = hits
|
||||||
// Check feasibility
|
// Check feasibility
|
||||||
.filter((hit: Intersection<Object3D>) => {
|
.filter((hit: Intersection<Object3D>) => {
|
||||||
@@ -106,7 +129,7 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
(isFace && selectFilter.value === '(F)aces') ||
|
(isFace && selectFilter.value === '(F)aces') ||
|
||||||
(isEdge && selectFilter.value === '(E)dges') ||
|
(isEdge && selectFilter.value === '(E)dges') ||
|
||||||
(isVertex && selectFilter.value === '(V)ertices');
|
(isVertex && selectFilter.value === '(V)ertices');
|
||||||
return (!isFace || hit.object.visible) && !hit.object.userData.noHit && kindOk;
|
return (!isFace || hit.object.visible) && kindOk;
|
||||||
})
|
})
|
||||||
// Sort for highlighting partially hidden edges/vertices
|
// Sort for highlighting partially hidden edges/vertices
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@@ -123,17 +146,19 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
})
|
})
|
||||||
// Return the best hit
|
// Return the best hit
|
||||||
[0] as Intersection<MObject3D> | undefined;
|
[0] as Intersection<MObject3D> | undefined;
|
||||||
// console.log('Hit', hit)
|
|
||||||
|
|
||||||
if (!highlightNextSelection.value[0]) {
|
if (!highlightNextSelection.value[0]) {
|
||||||
// If we are selecting, toggle the selection or deselect all if no hit
|
// If we are selecting, toggle the selection or deselect all if no hit
|
||||||
if (hit) {
|
let selInfo: SelectionInfo | null = null;
|
||||||
|
if (hit) selInfo = hitToSelectionInfo(hit);
|
||||||
|
//console.log('Hit', hit, 'SelInfo', selInfo);
|
||||||
|
if (hit && selInfo !== null) {
|
||||||
// Toggle selection
|
// Toggle selection
|
||||||
const wasSelected = selected.value.find((m) => m.object.name === hit?.object?.name) !== undefined;
|
const wasSelected = selected.value.find((m) => m.getKey() === selInfo.getKey()) !== undefined;
|
||||||
if (wasSelected) {
|
if (wasSelected) {
|
||||||
deselect(hit)
|
deselect(selInfo)
|
||||||
} else {
|
} else {
|
||||||
select(hit)
|
select(selInfo)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
deselectAll();
|
deselectAll();
|
||||||
@@ -149,34 +174,22 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
scene.queueRender() // Force rerender of model-viewer
|
scene.queueRender() // Force rerender of model-viewer
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(hit: Intersection<MObject3D>) {
|
function select(selInfo: SelectionInfo) {
|
||||||
// console.log('Selecting', hit.object.name)
|
// console.log('Selecting', selInfo.object.name)
|
||||||
if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) {
|
if (selected.value.find((m) => m.getKey() === selInfo.getKey()) === undefined) {
|
||||||
selected.value.push(hit);
|
selected.value.push(selInfo);
|
||||||
}
|
}
|
||||||
hit.object.material.__prevBaseColorFactor = [
|
highlight(selInfo);
|
||||||
hit.object.material.color.r,
|
|
||||||
hit.object.material.color.g,
|
|
||||||
hit.object.material.color.b,
|
|
||||||
];
|
|
||||||
hit.object.material.color.r = 1;
|
|
||||||
hit.object.material.color.g = 0;
|
|
||||||
hit.object.material.color.b = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deselect(hit: Intersection<MObject3D>, alsoRemove = true) {
|
function deselect(selInfo: SelectionInfo, alsoRemove = true) {
|
||||||
// console.log('Deselecting', hit.object.name)
|
// console.log('Deselecting', selInfo.object.name)
|
||||||
if (alsoRemove) {
|
if (alsoRemove) {
|
||||||
// Remove the matching object from the selection
|
// Remove the matching object from the selection
|
||||||
let toRemove = selected.value.findIndex((m) => m.object.name === hit.object.name);
|
let toRemove = selected.value.findIndex((m) => m.getKey() === selInfo.getKey());
|
||||||
selected.value.splice(toRemove, 1);
|
selected.value.splice(toRemove, 1);
|
||||||
}
|
}
|
||||||
if (hit.object.material.__prevBaseColorFactor) {
|
highlightUndo(selInfo);
|
||||||
hit.object.material.color.r = hit.object.material.__prevBaseColorFactor[0]
|
|
||||||
hit.object.material.color.g = hit.object.material.__prevBaseColorFactor[1]
|
|
||||||
hit.object.material.color.b = hit.object.material.__prevBaseColorFactor[2]
|
|
||||||
delete hit.object.material.__prevBaseColorFactor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deselectAll(alsoRemove = true) {
|
function deselectAll(alsoRemove = true) {
|
||||||
@@ -273,9 +286,8 @@ function updateBoundingBox() {
|
|||||||
if (selected.value.length > 0) {
|
if (selected.value.length > 0) {
|
||||||
bb = new Box3();
|
bb = new Box3();
|
||||||
for (let hit of selected.value) {
|
for (let hit of selected.value) {
|
||||||
bb.expandByObject(hit.object);
|
bb.union(hit.getBox())
|
||||||
}
|
}
|
||||||
bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget()));
|
|
||||||
} else {
|
} else {
|
||||||
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
|
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
|
||||||
if (!boundingBox) return; // No models. Should not happen.
|
if (!boundingBox) return; // No models. Should not happen.
|
||||||
@@ -380,9 +392,7 @@ function updateDistances() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add lines (if not already added)
|
// Add lines (if not already added)
|
||||||
let objA = selected.value[0].object;
|
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
|
||||||
let objB = selected.value[1].object;
|
|
||||||
let {min, center, max} = distances(objA, objB, props.viewer?.scene);
|
|
||||||
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
|
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(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");
|
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
|
|||||||
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
||||||
import SvgIcon from '@jamescoyle/vue-icon';
|
import SvgIcon from '@jamescoyle/vue-icon';
|
||||||
import type {ModelViewerElement} from '@google/model-viewer';
|
import type {ModelViewerElement} from '@google/model-viewer';
|
||||||
import type {Intersection} from "three";
|
|
||||||
import type {MObject3D} from "./Selection.vue";
|
import type {MObject3D} from "./Selection.vue";
|
||||||
import Loading from "../misc/Loading.vue";
|
import Loading from "../misc/Loading.vue";
|
||||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {defineAsyncComponent, type Ref, ref} from "vue";
|
import {defineAsyncComponent, type Ref, ref} from "vue";
|
||||||
|
import type {SelectionInfo} from "./selection";
|
||||||
|
|
||||||
const SelectionComponent = defineAsyncComponent({
|
const SelectionComponent = defineAsyncComponent({
|
||||||
loader: () => import("./Selection.vue"),
|
loader: () => import("./Selection.vue"),
|
||||||
@@ -39,10 +39,10 @@ const LicensesDialogContent = defineAsyncComponent({
|
|||||||
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
|
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
|
||||||
const emit = defineEmits<{ findModel: [string] }>()
|
const emit = defineEmits<{ findModel: [string] }>()
|
||||||
|
|
||||||
let selection: Ref<Array<Intersection<MObject3D>>> = ref([]);
|
let selection: Ref<Array<SelectionInfo>> = ref([]);
|
||||||
let selectionFaceCount = () => selection.value.filter((s) => s.object.type == "Mesh" || s.object.type == "SkinnedMesh").length
|
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
|
||||||
let selectionEdgeCount = () => selection.value.filter((s) => s.object.type == "Line").length
|
let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
|
||||||
let selectionVertexCount = () => selection.value.filter((s) => s.object.type == "Points").length
|
let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
|
||||||
|
|
||||||
function syncOrthoCamera(force: boolean) {
|
function syncOrthoCamera(force: boolean) {
|
||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
|
|||||||
151
frontend/tools/selection.ts
Normal file
151
frontend/tools/selection.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Model management from the graphics side
|
||||||
|
|
||||||
|
import type {MObject3D} from "./Selection.vue";
|
||||||
|
import type {Intersection} from "three";
|
||||||
|
import {Box3} from "three";
|
||||||
|
|
||||||
|
/** Information about a single item in the selection */
|
||||||
|
export class SelectionInfo {
|
||||||
|
/** The object which was (partially) selected */
|
||||||
|
object: MObject3D
|
||||||
|
/** The type of the selection */
|
||||||
|
kind: 'face' | 'edge' | 'vertex'
|
||||||
|
/** Start and end indices of the primitives in the geometry */
|
||||||
|
indices: [number, number]
|
||||||
|
|
||||||
|
constructor(object: MObject3D, kind: 'face' | 'edge' | 'vertex', indices: [number, number]) {
|
||||||
|
this.object = object;
|
||||||
|
this.kind = kind;
|
||||||
|
this.indices = indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getKey() {
|
||||||
|
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBox(): Box3 {
|
||||||
|
let index = this.object.geometry.index || {getX: (i: number) => i};
|
||||||
|
let pos = this.object.geometry.getAttribute('position');
|
||||||
|
let min = [Infinity, Infinity, Infinity];
|
||||||
|
let max = [-Infinity, -Infinity, -Infinity];
|
||||||
|
for (let i = this.indices[0]; i < this.indices[1]; i++) {
|
||||||
|
let vertIndex = index!.getX(i);
|
||||||
|
let x = pos.getX(vertIndex);
|
||||||
|
let y = pos.getY(vertIndex);
|
||||||
|
let z = pos.getZ(vertIndex);
|
||||||
|
min[0] = Math.min(min[0], x);
|
||||||
|
min[1] = Math.min(min[1], y);
|
||||||
|
min[2] = Math.min(min[2], z);
|
||||||
|
max[0] = Math.max(max[0], x);
|
||||||
|
max[1] = Math.max(max[1], y);
|
||||||
|
max[2] = Math.max(max[2], z);
|
||||||
|
}
|
||||||
|
return new Box3().setFromArray([...min, ...max]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hitToSelectionInfo(hit: Intersection<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()];
|
||||||
|
}
|
||||||
@@ -8,8 +8,16 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc
|
|||||||
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
||||||
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
||||||
import type {Vector3} from "three";
|
import type {Vector3} from "three";
|
||||||
|
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
|
||||||
|
import {BufferGeometry, Mesh} from "three";
|
||||||
|
|
||||||
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
||||||
|
//@ts-ignore
|
||||||
|
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
||||||
|
//@ts-ignore
|
||||||
|
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
||||||
|
//@ts-ignore
|
||||||
|
Mesh.prototype.raycast = acceleratedRaycast;
|
||||||
|
|
||||||
const emit = defineEmits<{ load: [] }>()
|
const emit = defineEmits<{ load: [] }>()
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,16 @@
|
|||||||
"@jamescoyle/vue-icon": "^0.1.2",
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/svg": "^7.4.47",
|
"@mdi/svg": "^7.4.47",
|
||||||
"three": "^0.160.1",
|
"three": "^0.162.0",
|
||||||
|
"three-mesh-bvh": "^0.7.3",
|
||||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vuetify": "^3.5.11"
|
"vuetify": "^3.5.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.2",
|
"@tsconfig/node20": "^20.1.3",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/three": "^0.160.0",
|
"@types/three": "^0.162.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
"terser": "^5.29.2",
|
"terser": "^5.29.2",
|
||||||
"typescript": "~5.4.3",
|
"typescript": "~5.4.3",
|
||||||
"vite": "^5.2.3",
|
"vite": "^5.2.6",
|
||||||
"vue-tsc": "^2.0.7"
|
"vue-tsc": "^2.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
assetsDir: '.',
|
assetsDir: '.',
|
||||||
cssCodeSplit: false, // Small enough to inline
|
cssCodeSplit: false, // Small enough to inline
|
||||||
|
chunkSizeWarningLimit: 550, // Three.js is huge
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__APP_NAME__: JSON.stringify(name),
|
__APP_NAME__: JSON.stringify(name),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
|
|||||||
# Expose some nice aliases using the default server instance
|
# Expose some nice aliases using the default server instance
|
||||||
show = yacv.show
|
show = yacv.show
|
||||||
show_all = yacv.show_cad_all
|
show_all = yacv.show_cad_all
|
||||||
prepare_image = image_to_gltf
|
image_to_gltf = image_to_gltf
|
||||||
export_all = yacv.export_all
|
export_all = yacv.export_all
|
||||||
remove = yacv.remove
|
remove = yacv.remove
|
||||||
clear = yacv.clear
|
clear = yacv.clear
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ def grab_all_cad() -> List[Tuple[str, CADCoreLike]]:
|
|||||||
|
|
||||||
|
|
||||||
def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None,
|
def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None,
|
||||||
name: Optional[str] = None, save_mime: str = 'image/jpeg') -> Tuple[bytes, str]:
|
name: Optional[str] = None, save_mime: str = 'image/jpeg', power_of_two: bool = True) \
|
||||||
|
-> Tuple[bytes, str]:
|
||||||
"""Convert an image to a GLTF CAD object."""
|
"""Convert an image to a GLTF CAD object."""
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
@@ -101,17 +102,27 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
|||||||
# Load the image to a byte buffer
|
# Load the image to a byte buffer
|
||||||
img = Image.open(source)
|
img = Image.open(source)
|
||||||
img_buf = io.BytesIO()
|
img_buf = io.BytesIO()
|
||||||
img.save(img_buf, format=format)
|
|
||||||
img_buf = img_buf.getvalue()
|
|
||||||
|
|
||||||
# Build the gltf
|
# Use the original dimensions for scaling the model
|
||||||
mgr = GLTFMgr(image=(img_buf, save_mime))
|
|
||||||
if width is None and height is None:
|
if width is None and height is None:
|
||||||
raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm?
|
raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm?
|
||||||
elif width is None:
|
elif width is None:
|
||||||
width = img.width / img.height * height
|
width = img.width / img.height * height
|
||||||
elif height is None:
|
elif height is None:
|
||||||
height = height or img.height / img.width * width # Apply default aspect ratio if unspecified
|
height = height or img.height / img.width * width # Apply default aspect ratio if unspecified
|
||||||
|
|
||||||
|
# Resize the image to a power of two if requested (recommended for GLTF)
|
||||||
|
if power_of_two:
|
||||||
|
new_width = 2 ** (img.width - 1).bit_length()
|
||||||
|
new_height = 2 ** (img.height - 1).bit_length()
|
||||||
|
img = img.resize((new_width, new_height))
|
||||||
|
|
||||||
|
# Save the image to a buffer
|
||||||
|
img.save(img_buf, format=format)
|
||||||
|
img_buf = img_buf.getvalue()
|
||||||
|
|
||||||
|
# Build the gltf
|
||||||
|
mgr = GLTFMgr(image=(img_buf, save_mime))
|
||||||
mgr.add_face([
|
mgr.add_face([
|
||||||
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||||
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||||
@@ -125,7 +136,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
|||||||
(1, 0),
|
(1, 0),
|
||||||
(1, 1),
|
(1, 1),
|
||||||
(0, 1),
|
(0, 1),
|
||||||
])
|
], (1, 1, 1, 1))
|
||||||
|
|
||||||
# Return the GLTF binary blob and the suggested name of the image
|
# Return the GLTF binary blob and the suggested name of the image
|
||||||
return b''.join(mgr.gltf.save_to_bytes()), name
|
return b''.join(mgr.build().save_to_bytes()), name
|
||||||
|
|||||||
@@ -12,45 +12,95 @@ _checkerboard_image_bytes = base64.decodebytes(
|
|||||||
class GLTFMgr:
|
class GLTFMgr:
|
||||||
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
||||||
|
|
||||||
def __init__(self, image: Tuple[bytes, str] = (_checkerboard_image_bytes, 'image/png')):
|
gltf: GLTF2
|
||||||
|
|
||||||
|
# Intermediate data to be filled by the add_* methods and merged into the GLTF object
|
||||||
|
# - Face data
|
||||||
|
face_indices: List[int] # 3 indices per triangle
|
||||||
|
face_positions: List[float] # x, y, z
|
||||||
|
face_tex_coords: List[float] # u, v
|
||||||
|
face_colors: List[float] # r, g, b, a
|
||||||
|
image: Optional[Tuple[bytes, str]] # image/png
|
||||||
|
# - Edge data
|
||||||
|
edge_indices: List[int] # 2 indices per edge
|
||||||
|
edge_positions: List[float] # x, y, z
|
||||||
|
edge_colors: List[float] # r, g, b, a
|
||||||
|
# - Vertex data
|
||||||
|
vertex_indices: List[int] # 1 index per vertex
|
||||||
|
vertex_positions: List[float] # x, y, z
|
||||||
|
vertex_colors: List[float] # r, g, b, a
|
||||||
|
|
||||||
|
def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')):
|
||||||
self.gltf = GLTF2(
|
self.gltf = GLTF2(
|
||||||
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
||||||
scene=0,
|
scene=0,
|
||||||
scenes=[Scene(nodes=[0])],
|
scenes=[Scene(nodes=[0])],
|
||||||
nodes=[Node(mesh=0)],
|
nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
|
||||||
meshes=[Mesh(primitives=[])],
|
meshes=[Mesh(primitives=[
|
||||||
accessors=[],
|
Primitive(indices=-1, attributes=Attributes(), mode=TRIANGLES, material=0,
|
||||||
bufferViews=[BufferView(buffer=0, byteLength=len(image[0]), byteOffset=0)],
|
extras={"face_triangles_end": []}),
|
||||||
buffers=[Buffer(byteLength=len(image[0]))],
|
Primitive(indices=-1, attributes=Attributes(), mode=LINES, material=0,
|
||||||
samplers=[Sampler(magFilter=NEAREST)],
|
extras={"edge_points_end": []}),
|
||||||
textures=[Texture(source=0, sampler=0)],
|
Primitive(indices=-1, attributes=Attributes(), mode=POINTS, material=0),
|
||||||
images=[Image(bufferView=0, mimeType=image[1])],
|
])],
|
||||||
|
materials=[Material(pbrMetallicRoughness=PbrMetallicRoughness(metallicFactor=0.1, roughnessFactor=1.0),
|
||||||
|
alphaCutoff=None)],
|
||||||
)
|
)
|
||||||
# TODO: Reduce the number of draw calls by merging all faces into a single primitive, and using
|
self.face_indices = []
|
||||||
# color attributes + extension? to differentiate them (same for edges and vertices)
|
self.face_positions = []
|
||||||
self.gltf.set_binary_blob(image[0])
|
self.face_tex_coords = []
|
||||||
|
self.face_colors = []
|
||||||
|
self.image = image
|
||||||
|
self.edge_indices = []
|
||||||
|
self.edge_positions = []
|
||||||
|
self.edge_colors = []
|
||||||
|
self.vertex_indices = []
|
||||||
|
self.vertex_positions = []
|
||||||
|
self.vertex_colors = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _faces_primitive(self) -> Primitive:
|
||||||
|
return [p for p in self.gltf.meshes[0].primitives if p.mode == TRIANGLES][0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _edges_primitive(self) -> Primitive:
|
||||||
|
return [p for p in self.gltf.meshes[0].primitives if p.mode == LINES][0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _vertices_primitive(self) -> Primitive:
|
||||||
|
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
|
||||||
|
|
||||||
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
||||||
tex_coord_raw: List[Tuple[float, float]]):
|
tex_coord_raw: List[Tuple[float, float]],
|
||||||
"""Add a face to the GLTF as a new primitive of the unique mesh"""
|
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
|
||||||
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
"""Add a face to the GLTF mesh"""
|
||||||
indices = np.array([[i[0], i[1], i[2]] for i in indices_raw], dtype=np.uint32)
|
# assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths"
|
||||||
tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32)
|
# assert min([i for t in indices_raw for i in t]) == 0, f"Face indices start at {min(indices_raw)}"
|
||||||
self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material="face")
|
# assert max([e for t in indices_raw for e in t]) < len(vertices_raw), f"Indices have non-existing vertices"
|
||||||
|
base_index = len(self.face_positions) // 3 # All the new indices reference the new vertices
|
||||||
|
self.face_indices.extend([base_index + i for t in indices_raw for i in t])
|
||||||
|
self.face_positions.extend([v for t in vertices_raw for v in t])
|
||||||
|
self.face_tex_coords.extend([c for t in tex_coord_raw for c in t])
|
||||||
|
self.face_colors.extend([col for _ in range(len(vertices_raw)) for col in color])
|
||||||
|
self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices))
|
||||||
|
|
||||||
def add_edge(self, vertices_raw: List[Tuple[float, float, float]], mat: str = None):
|
def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
|
||||||
"""Add an edge to the GLTF as a new primitive of the unique mesh"""
|
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.4, 1.0)):
|
||||||
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
"""Add an edge to the GLTF mesh"""
|
||||||
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32)
|
vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc.
|
||||||
tex_coord = np.array([])
|
base_index = len(self.edge_positions) // 3
|
||||||
self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material=mat or "edge")
|
self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))])
|
||||||
|
self.edge_positions.extend([v for t in vertices_flat for v in t])
|
||||||
|
self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color])
|
||||||
|
self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices))
|
||||||
|
|
||||||
def add_vertex(self, vertex: Tuple[float, float, float]):
|
def add_vertex(self, vertex: Tuple[float, float, float],
|
||||||
"""Add a vertex to the GLTF as a new primitive of the unique mesh"""
|
color: Tuple[float, float, float, float] = (0.1, 0.4, 0.1, 1.0)):
|
||||||
vertices = np.array([[vertex[0], vertex[1], vertex[2]]])
|
"""Add a vertex to the GLTF mesh"""
|
||||||
indices = np.array([[0]], dtype=np.uint32)
|
base_index = len(self.vertex_positions) // 3
|
||||||
tex_coord = np.array([], dtype=np.float32)
|
self.vertex_indices.append(base_index)
|
||||||
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex")
|
self.vertex_positions.extend(vertex)
|
||||||
|
self.vertex_colors.extend(color)
|
||||||
|
|
||||||
def add_location(self, loc: Location):
|
def add_location(self, loc: Location):
|
||||||
"""Add a location to the GLTF as a new primitive of the unique mesh"""
|
"""Add a location to the GLTF as a new primitive of the unique mesh"""
|
||||||
@@ -61,120 +111,91 @@ class GLTFMgr:
|
|||||||
|
|
||||||
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
|
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
|
||||||
self.add_vertex(vert(pl.origin))
|
self.add_vertex(vert(pl.origin))
|
||||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.x_dir)], mat="locX")
|
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1))
|
||||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.y_dir)], mat="locY")
|
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1))
|
||||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.z_dir)], mat="locZ")
|
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1))
|
||||||
|
|
||||||
def add_material(self, kind: str) -> int:
|
def build(self) -> GLTF2:
|
||||||
"""It is important to use a different material for each primitive to be able to change them at runtime"""
|
"""Merge the intermediate data into the GLTF object and return it"""
|
||||||
new_material: Material
|
buffers_list: List[Tuple[Accessor, BufferView, bytes]] = []
|
||||||
if kind == "face":
|
|
||||||
new_material = Material(name="face", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
if len(self.face_indices) > 0:
|
||||||
baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 1, 0.5, 1]), doubleSided=True)
|
self._faces_primitive.indices = len(buffers_list)
|
||||||
elif kind == "edge":
|
buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
|
||||||
new_material = Material(name="edge", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
self._faces_primitive.attributes.POSITION = len(buffers_list)
|
||||||
baseColorFactor=[0, 0, 0.5, 1]))
|
buffers_list.append(_gen_buffer_metadata(self.face_positions, 3))
|
||||||
elif kind == "vertex":
|
self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
|
||||||
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
|
||||||
baseColorFactor=[0, 0.3, 0.3, 1]))
|
self._faces_primitive.attributes.COLOR_0 = len(buffers_list)
|
||||||
elif kind == "locX":
|
buffers_list.append(_gen_buffer_metadata(self.face_colors, 4))
|
||||||
new_material = Material(name="locX", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
|
||||||
baseColorFactor=[0.97, 0.24, 0.24, 1]))
|
|
||||||
elif kind == "locY":
|
|
||||||
new_material = Material(name="locY", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
|
||||||
baseColorFactor=[0.42, 0.8, 0.15, 1]))
|
|
||||||
elif kind == "locZ":
|
|
||||||
new_material = Material(name="locZ", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
|
||||||
baseColorFactor=[0.09, 0.55, 0.94, 1]))
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown material kind {kind}")
|
self.image = None # Unused image
|
||||||
self.gltf.materials.append(new_material)
|
self.gltf.meshes[0].primitives = list( # Remove unused faces primitive
|
||||||
return len(self.gltf.materials) - 1
|
filter(lambda p: p.mode != TRIANGLES, self.gltf.meshes[0].primitives))
|
||||||
|
|
||||||
def _add_any(self, vertices: np.ndarray, indices: np.ndarray, tex_coord: np.ndarray, mode: int = TRIANGLES,
|
edges_and_vertices_mat = 0
|
||||||
material: str = "face"):
|
if self.image is not None and (len(self.edge_indices) > 0 or len(self.vertex_indices) > 0):
|
||||||
assert vertices.ndim == 2
|
# Create a material without texture for edges and vertices
|
||||||
assert vertices.shape[1] == 3
|
edges_and_vertices_mat = len(self.gltf.materials)
|
||||||
vertices = vertices.astype(np.float32)
|
new_mat = copy.deepcopy(self.gltf.materials[0])
|
||||||
vertices_blob = vertices.tobytes()
|
new_mat.pbrMetallicRoughness.baseColorTexture = None
|
||||||
|
self.gltf.materials.append(new_mat)
|
||||||
|
|
||||||
assert indices.ndim == 2
|
# Treat edges and vertices the same way
|
||||||
assert indices.shape[1] == 3 and mode == TRIANGLES or indices.shape[1] == 2 and mode == LINE_STRIP or \
|
for (indices, positions, colors, primitive, kind) in [
|
||||||
indices.shape[1] == 1 and mode == POINTS
|
(self.edge_indices, self.edge_positions, self.edge_colors, self._edges_primitive, LINES),
|
||||||
indices = indices.astype(np.uint32)
|
(self.vertex_indices, self.vertex_positions, self.vertex_colors, self._vertices_primitive, POINTS)
|
||||||
indices_blob = indices.flatten().tobytes()
|
]:
|
||||||
|
if len(indices) > 0:
|
||||||
|
primitive.material = edges_and_vertices_mat
|
||||||
|
primitive.indices = len(buffers_list)
|
||||||
|
buffers_list.append(_gen_buffer_metadata(indices, 1))
|
||||||
|
primitive.attributes.POSITION = len(buffers_list)
|
||||||
|
buffers_list.append(_gen_buffer_metadata(positions, 3))
|
||||||
|
primitive.attributes.COLOR_0 = len(buffers_list)
|
||||||
|
buffers_list.append(_gen_buffer_metadata(colors, 4))
|
||||||
|
else:
|
||||||
|
self.gltf.meshes[0].primitives = list( # Remove unused edges primitive
|
||||||
|
filter(lambda p: p.mode != kind, self.gltf.meshes[0].primitives))
|
||||||
|
|
||||||
# Check that all vertices are referenced by the indices
|
if self.image is not None: # Add texture last as it creates a fake accessor that is not added!
|
||||||
# This can happen on broken faces like on some fonts
|
self.gltf.images = [Image(bufferView=len(buffers_list), mimeType=self.image[1])]
|
||||||
# assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}"
|
self.gltf.textures = [Texture(source=0, sampler=0)]
|
||||||
# assert indices.min() == 0, f"min({indices}) != 0"
|
self.gltf.samplers = [Sampler(magFilter=NEAREST)]
|
||||||
# assert np.unique(indices.flatten()).size == len(vertices)
|
self.gltf.materials[0].pbrMetallicRoughness.baseColorTexture = TextureInfo(index=0)
|
||||||
|
buffers_list.append((Accessor(), BufferView(), self.image[0]))
|
||||||
|
|
||||||
assert len(tex_coord) == 0 or tex_coord.ndim == 2
|
# Once all the data is ready, we can concatenate the buffers updating the accessors and views
|
||||||
assert len(tex_coord) == 0 or tex_coord.shape[1] == 2
|
prev_binary_blob = self.gltf.binary_blob() or b''
|
||||||
tex_coord = tex_coord.astype(np.float32)
|
|
||||||
tex_coord_blob = tex_coord.tobytes()
|
|
||||||
|
|
||||||
accessor_base = len(self.gltf.accessors)
|
|
||||||
self.gltf.meshes[0].primitives.append(
|
|
||||||
Primitive(
|
|
||||||
attributes=Attributes(POSITION=accessor_base + 1, TEXCOORD_0=accessor_base + 2)
|
|
||||||
if len(tex_coord) > 0 else Attributes(POSITION=accessor_base + 1),
|
|
||||||
indices=accessor_base,
|
|
||||||
mode=mode,
|
|
||||||
material=self.add_material(material),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
buffer_view_base = len(self.gltf.bufferViews)
|
|
||||||
self.gltf.accessors.extend([it for it in [
|
|
||||||
Accessor(
|
|
||||||
bufferView=buffer_view_base,
|
|
||||||
componentType=UNSIGNED_INT,
|
|
||||||
count=indices.size,
|
|
||||||
type=SCALAR,
|
|
||||||
max=[int(indices.max())],
|
|
||||||
min=[int(indices.min())],
|
|
||||||
),
|
|
||||||
Accessor(
|
|
||||||
bufferView=buffer_view_base + 1,
|
|
||||||
componentType=FLOAT,
|
|
||||||
count=len(vertices),
|
|
||||||
type=VEC3,
|
|
||||||
max=vertices.max(axis=0).tolist(),
|
|
||||||
min=vertices.min(axis=0).tolist(),
|
|
||||||
),
|
|
||||||
Accessor(
|
|
||||||
bufferView=buffer_view_base + 2,
|
|
||||||
componentType=FLOAT,
|
|
||||||
count=len(tex_coord),
|
|
||||||
type=VEC2,
|
|
||||||
max=tex_coord.max(axis=0).tolist(),
|
|
||||||
min=tex_coord.min(axis=0).tolist(),
|
|
||||||
) if len(tex_coord) > 0 else None
|
|
||||||
] if it is not None])
|
|
||||||
|
|
||||||
prev_binary_blob = self.gltf.binary_blob()
|
|
||||||
byte_offset_base = len(prev_binary_blob)
|
byte_offset_base = len(prev_binary_blob)
|
||||||
self.gltf.bufferViews.extend([bv for bv in [
|
for accessor, bufferView, blob in buffers_list:
|
||||||
BufferView(
|
|
||||||
buffer=0,
|
|
||||||
byteOffset=byte_offset_base,
|
|
||||||
byteLength=len(indices_blob),
|
|
||||||
target=ELEMENT_ARRAY_BUFFER,
|
|
||||||
),
|
|
||||||
BufferView(
|
|
||||||
buffer=0,
|
|
||||||
byteOffset=byte_offset_base + len(indices_blob),
|
|
||||||
byteLength=len(vertices_blob),
|
|
||||||
target=ARRAY_BUFFER,
|
|
||||||
),
|
|
||||||
BufferView(
|
|
||||||
buffer=0,
|
|
||||||
byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob),
|
|
||||||
byteLength=len(tex_coord_blob),
|
|
||||||
target=ARRAY_BUFFER,
|
|
||||||
)
|
|
||||||
] if bv.byteLength > 0])
|
|
||||||
|
|
||||||
self.gltf.set_binary_blob(prev_binary_blob + indices_blob + vertices_blob + tex_coord_blob)
|
if accessor.componentType is not None: # Remove accessor of texture
|
||||||
|
buffer_view_base = len(self.gltf.bufferViews)
|
||||||
|
accessor.bufferView = buffer_view_base
|
||||||
|
self.gltf.accessors.append(accessor)
|
||||||
|
|
||||||
|
bufferView.buffer = 0
|
||||||
|
bufferView.byteOffset = byte_offset_base
|
||||||
|
bufferView.byteLength = len(blob)
|
||||||
|
self.gltf.bufferViews.append(bufferView)
|
||||||
|
|
||||||
|
byte_offset_base += len(blob)
|
||||||
|
prev_binary_blob += blob
|
||||||
|
|
||||||
|
self.gltf.buffers.append(Buffer(byteLength=byte_offset_base))
|
||||||
|
self.gltf.set_binary_blob(prev_binary_blob)
|
||||||
|
|
||||||
|
return self.gltf
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_buffer_metadata(data: List[any], chunk: int) -> Tuple[Accessor, BufferView, bytes]:
|
||||||
|
return Accessor(
|
||||||
|
componentType={1: UNSIGNED_INT, 2: FLOAT, 3: FLOAT, 4: FLOAT}[chunk],
|
||||||
|
count=len(data) // chunk,
|
||||||
|
type={1: SCALAR, 2: VEC2, 3: VEC3, 4: VEC4}[chunk],
|
||||||
|
max=[max(data[i::chunk]) for i in range(chunk)],
|
||||||
|
min=[min(data[i::chunk]) for i in range(chunk)],
|
||||||
|
), BufferView(
|
||||||
|
target={1: ELEMENT_ARRAY_BUFFER, 2: ARRAY_BUFFER, 3: ARRAY_BUFFER, 4: ARRAY_BUFFER}[chunk],
|
||||||
|
), np.array(data, dtype={1: np.uint32, 2: np.float32, 3: np.float32, 4: np.float32}[chunk]).tobytes()
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
|
|||||||
logo_img_location.position.Z)
|
logo_img_location.position.Z)
|
||||||
|
|
||||||
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
|
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
|
||||||
img_bytes, img_name = prepare_image(logo_img_path, logo_img_location, height=18)
|
img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
|
||||||
|
|
||||||
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read()
|
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read()
|
||||||
|
|
||||||
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_bytes}
|
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_glb_bytes}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -43,7 +43,7 @@ if __name__ == "__main__":
|
|||||||
# If this is not set, the server will auto-start on import and show_* calls will provide live updates
|
# If this is not set, the server will auto-start on import and show_* calls will provide live updates
|
||||||
os.environ['YACV_DISABLE_SERVER'] = 'True'
|
os.environ['YACV_DISABLE_SERVER'] = 'True'
|
||||||
|
|
||||||
from yacv_server import export_all, remove, prepare_image, show
|
from yacv_server import export_all, show, image_to_gltf
|
||||||
|
|
||||||
# Build the CAD part of the logo
|
# Build the CAD part of the logo
|
||||||
logo = build_logo()
|
logo = build_logo()
|
||||||
@@ -52,7 +52,8 @@ if __name__ == "__main__":
|
|||||||
show(*[obj for obj in logo.values()], names=[name for name in logo.keys()])
|
show(*[obj for obj in logo.values()], names=[name for name in logo.keys()])
|
||||||
|
|
||||||
if testing_server:
|
if testing_server:
|
||||||
remove('location') # Test removing a part
|
# remove('location') # Test removing a part
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
# Save the complete logo to multiple GLB files
|
# Save the complete logo to multiple GLB files
|
||||||
export_all(os.path.join(ASSETS_DIR, 'logo_build'))
|
export_all(os.path.join(ASSETS_DIR, 'logo_build'))
|
||||||
|
|||||||
@@ -36,26 +36,26 @@ def tessellate(
|
|||||||
shape = Shape(cad_like)
|
shape = Shape(cad_like)
|
||||||
|
|
||||||
# Perform tessellation tasks
|
# Perform tessellation tasks
|
||||||
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
|
edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
|
||||||
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {}
|
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
|
||||||
if faces:
|
if faces:
|
||||||
for face in shape.faces():
|
for face in shape.faces():
|
||||||
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
|
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
|
||||||
if edges:
|
if edges:
|
||||||
for edge in face.edges():
|
for edge in face.edges():
|
||||||
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
|
edge_to_faces[_hashcode(edge.wrapped)] = edge_to_faces.get(_hashcode(edge.wrapped), []) + [face.wrapped]
|
||||||
if vertices:
|
if vertices:
|
||||||
for vertex in face.vertices():
|
for vertex in face.vertices():
|
||||||
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped]
|
vertex_to_faces[_hashcode(vertex.wrapped)] = vertex_to_faces.get(_hashcode(vertex.wrapped), []) + [face.wrapped]
|
||||||
if edges:
|
if edges:
|
||||||
for edge in shape.edges():
|
for edge in shape.edges():
|
||||||
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance,
|
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(_hashcode(edge.wrapped), []), angular_tolerance,
|
||||||
angular_tolerance)
|
angular_tolerance)
|
||||||
if vertices:
|
if vertices:
|
||||||
for vertex in shape.vertices():
|
for vertex in shape.vertices():
|
||||||
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
|
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(_hashcode(vertex.wrapped), []))
|
||||||
|
|
||||||
return mgr.gltf
|
return mgr.build()
|
||||||
|
|
||||||
|
|
||||||
def _tessellate_face(
|
def _tessellate_face(
|
||||||
@@ -91,9 +91,9 @@ def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tupl
|
|||||||
push_dir = (push_dir[0] + normal.X, push_dir[1] + normal.Y, push_dir[2] + normal.Z)
|
push_dir = (push_dir[0] + normal.X, push_dir[1] + normal.Y, push_dir[2] + normal.Z)
|
||||||
if push_dir != (0, 0, 0):
|
if push_dir != (0, 0, 0):
|
||||||
# Normalize the push direction by the number of faces and a constant factor
|
# Normalize the push direction by the number of faces and a constant factor
|
||||||
# NOTE: Don't overdo it, or metrics will be wrong
|
# NOTE: Don't overdo it, or metrics will be (more) wrong
|
||||||
n = len(faces) / 1e-3
|
n = 1e-3 / len(faces)
|
||||||
push_dir = (push_dir[0] / n, push_dir[1] / n, push_dir[2] / n)
|
push_dir = (push_dir[0] * n, push_dir[1] * n, push_dir[2] * n)
|
||||||
# Push the vertex by the normal
|
# Push the vertex by the normal
|
||||||
v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2])
|
v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2])
|
||||||
return v
|
return v
|
||||||
@@ -119,6 +119,9 @@ def _tessellate_edge(
|
|||||||
for i in range(1, discretizer.NbPoints() + 1)
|
for i in range(1, discretizer.NbPoints() + 1)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Convert strip of vertices to a list of pairs of vertices
|
||||||
|
vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)]
|
||||||
mgr.add_edge(vertices)
|
mgr.add_edge(vertices)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
43
yarn.lock
43
yarn.lock
@@ -777,10 +777,10 @@
|
|||||||
"@sigstore/core" "^1.0.0"
|
"@sigstore/core" "^1.0.0"
|
||||||
"@sigstore/protobuf-specs" "^0.3.0"
|
"@sigstore/protobuf-specs" "^0.3.0"
|
||||||
|
|
||||||
"@tsconfig/node20@^20.1.2":
|
"@tsconfig/node20@^20.1.3":
|
||||||
version "20.1.2"
|
version "20.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.2.tgz#b93128c411d38e9507035255195bc8a6718541e3"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.3.tgz#b3b4cf785e1b390a6ab48a68aa594a25960d2fe8"
|
||||||
integrity sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==
|
integrity sha512-XeWn6Gms5MaQWdj+C4fuxuo/Icy8ckh+BwAIijhX2LKRHHt1OuctLLLlB0F4EPi55m2IUJNTnv8FH9kSBI7Ogw==
|
||||||
|
|
||||||
"@tufjs/canonical-json@2.0.0":
|
"@tufjs/canonical-json@2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -795,6 +795,11 @@
|
|||||||
"@tufjs/canonical-json" "2.0.0"
|
"@tufjs/canonical-json" "2.0.0"
|
||||||
minimatch "^9.0.3"
|
minimatch "^9.0.3"
|
||||||
|
|
||||||
|
"@tweenjs/tween.js@~23.1.1":
|
||||||
|
version "23.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.1.tgz#0ae28ed9c635805557f78c2626464018d5f1b5e2"
|
||||||
|
integrity sha512-ZpboH7pCPPeyBWKf8c7TJswtCEQObFo3bOBYalm99NzZarATALYCo5OhbCa/n4RQyJyHfhkdx+hNrdL5ByFYDw==
|
||||||
|
|
||||||
"@types/estree@1.0.5":
|
"@types/estree@1.0.5":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||||
@@ -817,11 +822,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.3.tgz#705446e12ce0fad618557dd88236f51148b7a935"
|
resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.3.tgz#705446e12ce0fad618557dd88236f51148b7a935"
|
||||||
integrity sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==
|
integrity sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==
|
||||||
|
|
||||||
"@types/three@^0.160.0":
|
"@types/three@^0.162.0":
|
||||||
version "0.160.0"
|
version "0.162.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.160.0.tgz#7915a97e0a14ccaa9ccbb9f190c5730b04a23075"
|
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.162.0.tgz#79d170c88f14b2eaee6b76af00fc4016a533e586"
|
||||||
integrity sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w==
|
integrity sha512-0j5yZcVukVIhrhSIC7+LmBPkkMoMuEJ1AfYBZfgNytdYqYREMuiyXWhYOMeZLBElTEAlJIZn7r2W3vqTIgjWlg==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@tweenjs/tween.js" "~23.1.1"
|
||||||
"@types/stats.js" "*"
|
"@types/stats.js" "*"
|
||||||
"@types/webxr" "*"
|
"@types/webxr" "*"
|
||||||
fflate "~0.6.10"
|
fflate "~0.6.10"
|
||||||
@@ -2893,6 +2899,11 @@ terser@^5.29.2:
|
|||||||
commander "^2.20.0"
|
commander "^2.20.0"
|
||||||
source-map-support "~0.5.20"
|
source-map-support "~0.5.20"
|
||||||
|
|
||||||
|
three-mesh-bvh@^0.7.3:
|
||||||
|
version "0.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.3.tgz#91f2d7e26f230288b5b0a6bdf41bdd9620348945"
|
||||||
|
integrity sha512-3W6KjzmupjfE89GuHPT31kxKWZ4YGZPEZJNysJpiOZfQRsBQQgmK7v/VJPpjG6syhAvTnY+5Fr77EvIkTLpGSw==
|
||||||
|
|
||||||
"three-orientation-gizmo@https://github.com/jrj2211/three-orientation-gizmo":
|
"three-orientation-gizmo@https://github.com/jrj2211/three-orientation-gizmo":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://github.com/jrj2211/three-orientation-gizmo#000281f0559c316f72cdd23a1885d63ae6901095"
|
resolved "https://github.com/jrj2211/three-orientation-gizmo#000281f0559c316f72cdd23a1885d63ae6901095"
|
||||||
@@ -2904,10 +2915,10 @@ three@^0.125.0:
|
|||||||
resolved "https://registry.yarnpkg.com/three/-/three-0.125.2.tgz#dcba12749a2eb41522e15212b919cd3fbf729b12"
|
resolved "https://registry.yarnpkg.com/three/-/three-0.125.2.tgz#dcba12749a2eb41522e15212b919cd3fbf729b12"
|
||||||
integrity sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==
|
integrity sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==
|
||||||
|
|
||||||
three@^0.160.1:
|
three@^0.162.0:
|
||||||
version "0.160.1"
|
version "0.162.0"
|
||||||
resolved "https://registry.yarnpkg.com/three/-/three-0.160.1.tgz#61fe2907312e8604b1f64187f58e047503847413"
|
resolved "https://registry.yarnpkg.com/three/-/three-0.162.0.tgz#b15a511f1498e0c42d4d00bbb411c7527b06097e"
|
||||||
integrity sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==
|
integrity sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==
|
||||||
|
|
||||||
to-fast-properties@^2.0.0:
|
to-fast-properties@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -2997,10 +3008,10 @@ validate-npm-package-name@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
builtins "^5.0.0"
|
builtins "^5.0.0"
|
||||||
|
|
||||||
vite@^5.2.3:
|
vite@^5.2.6:
|
||||||
version "5.2.3"
|
version "5.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.3.tgz#198efc2fd4d80eac813b146a68a4b0dbde884fc2"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222"
|
||||||
integrity sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==
|
integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.20.1"
|
esbuild "^0.20.1"
|
||||||
postcss "^8.4.36"
|
postcss "^8.4.36"
|
||||||
|
|||||||
Reference in New Issue
Block a user