mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
add complete helpers support
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {Document, TypedArray} from '@gltf-transform/core'
|
||||
import {Matrix4, Vector3} from 'three'
|
||||
import {Matrix4, Vector2, Vector3} from 'three'
|
||||
|
||||
|
||||
/** Exports the colors used for the axes, primary and secondary. They match the orientation gizmo. */
|
||||
@@ -9,45 +9,55 @@ export const AxesColors = {
|
||||
y: [[23, 140, 240], [14, 84, 144]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
|
||||
*/
|
||||
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
const buffer = doc.createBuffer()
|
||||
const positions = doc.createAccessor('axesPosition')
|
||||
.setArray(new Float32Array([
|
||||
0, 0, 0,
|
||||
size.x, 0, 0,
|
||||
0, 0, 0,
|
||||
0, size.y, 0,
|
||||
0, 0, 0,
|
||||
0, 0, -size.z,
|
||||
]) as TypedArray)
|
||||
function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: number[], rawColors: number[] | null, transform: Matrix4, name: string = '__helper', mode: number = WebGL2RenderingContext.LINES) {
|
||||
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + 'Buffer')
|
||||
const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + 'Scene')
|
||||
const positions = doc.createAccessor(name + 'Position')
|
||||
.setArray(new Float32Array(rawPositions) as TypedArray)
|
||||
.setType('VEC3')
|
||||
.setBuffer(buffer)
|
||||
const indices = doc.createAccessor('axesIndices')
|
||||
.setArray(new Uint32Array([0, 1, 2, 3, 4, 5]) as TypedArray)
|
||||
const indices = doc.createAccessor(name + 'Indices')
|
||||
.setArray(new Uint32Array(rawIndices) as TypedArray)
|
||||
.setType('SCALAR')
|
||||
.setBuffer(buffer)
|
||||
const colors = doc.createAccessor('axesColor')
|
||||
.setArray(new Float32Array([
|
||||
...(AxesColors.x[0]), ...(AxesColors.x[1]),
|
||||
...(AxesColors.y[0]), ...(AxesColors.y[1]),
|
||||
...(AxesColors.z[0]), ...(AxesColors.z[1]),
|
||||
].map(x => x / 255.0)) as TypedArray)
|
||||
const colors = doc.createAccessor(name + 'Color')
|
||||
.setArray(new Float32Array(rawColors) as TypedArray)
|
||||
.setType('VEC3')
|
||||
.setBuffer(buffer)
|
||||
const material = doc.createMaterial('axesMaterial')
|
||||
const material = doc.createMaterial(name + 'Material')
|
||||
.setAlphaMode('OPAQUE')
|
||||
const geometry = doc.createPrimitive()
|
||||
.setIndices(indices)
|
||||
.setAttribute('POSITION', positions)
|
||||
.setAttribute('COLOR_0', colors)
|
||||
.setMode(WebGL2RenderingContext.LINES)
|
||||
.setMode(mode as any)
|
||||
.setMaterial(material)
|
||||
const mesh = doc.createMesh('axes').addPrimitive(geometry)
|
||||
const node = doc.createNode('axes').setMesh(mesh).setMatrix(transform.elements as any)
|
||||
doc.createScene('axesScene').addChild(node)
|
||||
if (rawColors) {
|
||||
geometry.setAttribute('COLOR_0', colors)
|
||||
}
|
||||
const mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
|
||||
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
|
||||
scene.addChild(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
|
||||
*/
|
||||
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
let rawPositions = [
|
||||
0, 0, 0,
|
||||
size.x, 0, 0,
|
||||
0, 0, 0,
|
||||
0, size.y, 0,
|
||||
0, 0, 0,
|
||||
0, 0, -size.z,
|
||||
];
|
||||
let rawIndices = [0, 1, 2, 3, 4, 5];
|
||||
let rawColors = [
|
||||
...(AxesColors.x[0]), ...(AxesColors.x[1]),
|
||||
...(AxesColors.y[0]), ...(AxesColors.y[1]),
|
||||
...(AxesColors.z[0]), ...(AxesColors.z[1]),
|
||||
].map(x => x / 255.0);
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +66,48 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
* The grid is built as a box of triangles (representing lines) looking to the inside of the box.
|
||||
* This ensures that only the back of the grid is always visible, regardless of the camera position.
|
||||
*/
|
||||
export function newGrid(doc: Document, size: Vector3, transform: Matrix4 = new Matrix4(), divisions = 10) {
|
||||
const buffer = doc.createBuffer();
|
||||
// TODO: implement grid
|
||||
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4 = new Matrix4(), divisions = 10) {
|
||||
// Create transformed positions for the inner faces of the box
|
||||
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
|
||||
for (let positive of [1, -1]) {
|
||||
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
|
||||
let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z)
|
||||
let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0))
|
||||
let size2 = new Vector2();
|
||||
if (axis.x) size2.set(size.z, size.y);
|
||||
if (axis.y) size2.set(size.x, size.z);
|
||||
if (axis.z) size2.set(size.x, size.y);
|
||||
let transform = baseTransform.clone().multiply(translation).multiply(rotation);
|
||||
newGridPlane(doc, size2, transform, divisions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.2) {
|
||||
const rawPositions = [];
|
||||
const rawIndices = [];
|
||||
// Build the grid as triangles
|
||||
for (let i = 0; i <= divisions; i++) {
|
||||
const x = -size.x / 2 + size.x * i / divisions;
|
||||
const y = -size.y / 2 + size.y * i / divisions;
|
||||
|
||||
// Vertical quad (two triangles)
|
||||
rawPositions.push(x - divisionWidth / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + divisionWidth / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + divisionWidth / 2, size.y / 2, 0);
|
||||
rawPositions.push(x - divisionWidth / 2, size.y / 2, 0);
|
||||
const baseIndex = i * 4;
|
||||
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
||||
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
||||
|
||||
// Horizontal quad (two triangles)
|
||||
rawPositions.push(-size.x / 2, y - divisionWidth / 2, 0);
|
||||
rawPositions.push(size.x / 2, y - divisionWidth / 2, 0);
|
||||
rawPositions.push(size.x / 2, y + divisionWidth / 2, 0);
|
||||
rawPositions.push(-size.x / 2, y + divisionWidth / 2, 0);
|
||||
const baseIndex2 = (divisions+1 + i) * 4;
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||
}
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Ref, ShallowRef} from 'vue';
|
||||
import {Document} from '@gltf-transform/core';
|
||||
import {mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
||||
import {newAxes} from "./helpers";
|
||||
import { Matrix4, Vector3 } from 'three';
|
||||
import {extrasNameKey, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
||||
import {newAxes, newGridBox} from "./helpers";
|
||||
import {Matrix4, Vector3} from 'three';
|
||||
|
||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||
export class SceneMgr {
|
||||
@@ -13,23 +13,49 @@ export class SceneMgr {
|
||||
// Start merging into the current document, replacing or adding as needed
|
||||
document.value = await mergePartial(url, name, document.value);
|
||||
|
||||
// Display the final fully loaded model
|
||||
await this.showCurrentDoc(sceneUrl, document);
|
||||
if (name !== "__helpers") {
|
||||
// Reload the helpers to fit the new model
|
||||
await this.reloadHelpers(sceneUrl, document);
|
||||
} else {
|
||||
// Display the final fully loaded model
|
||||
await this.showCurrentDoc(sceneUrl, document);
|
||||
}
|
||||
|
||||
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
|
||||
|
||||
if (name !== "__helpers") {
|
||||
// Add a helper axes to the scene
|
||||
let helpersDoc = new Document();
|
||||
// TODO: Get bounding box of the model and use it to set the size of the helpers
|
||||
newAxes(helpersDoc, new Vector3(10, 10, 10), new Matrix4());
|
||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
||||
await SceneMgr.loadModel(sceneUrl, document, "__helpers", helpersUrl);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static async reloadHelpers(sceneUrl: Ref<string>, document: ShallowRef<Document>) {
|
||||
// Get bounding box of the model and use it to set the size of the helpers
|
||||
let bbMin: number[] = [1e6, 1e6, 1e6];
|
||||
let bbMax: number[] = [-1e6, -1e6, -1e6];
|
||||
document.value.getRoot().listNodes().forEach(node => {
|
||||
if ((node.getExtras()[extrasNameKey] ?? "__helpers") === "__helpers") return;
|
||||
let transform = new Matrix4(...node.getWorldMatrix());
|
||||
for (let prim of node.getMesh()?.listPrimitives() ?? []) {
|
||||
let accessor = prim.getAttribute('POSITION');
|
||||
if (!accessor) continue;
|
||||
let objMin = new Vector3(...accessor.getMin([0, 0, 0]))
|
||||
.applyMatrix4(transform);
|
||||
let objMax = new Vector3(...accessor.getMax([0, 0, 0]))
|
||||
.applyMatrix4(transform);
|
||||
bbMin = bbMin.map((v, i) => Math.min(v, objMin.getComponent(i)));
|
||||
bbMax = bbMax.map((v, i) => Math.max(v, objMax.getComponent(i)));
|
||||
}
|
||||
});
|
||||
let bbSize = new Vector3().fromArray(bbMax).sub(new Vector3().fromArray(bbMin));
|
||||
let bbCenter = new Vector3().fromArray(bbMin).add(bbSize.clone().multiplyScalar(0.5));
|
||||
let bbTransform = new Matrix4().makeTranslation(bbCenter.x, bbCenter.y, bbCenter.z);
|
||||
|
||||
// Create the helper axes and grid box
|
||||
let helpersDoc = new Document();
|
||||
newAxes(helpersDoc, bbSize.clone().multiplyScalar(0.5), bbTransform);
|
||||
newGridBox(helpersDoc, bbSize, bbTransform);
|
||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
||||
await SceneMgr.loadModel(sceneUrl, document, "__helpers", helpersUrl);
|
||||
}
|
||||
|
||||
/** Removes a model from the viewer */
|
||||
static async removeModel(sceneUrl: Ref<string>, document: ShallowRef<Document>, name: string) {
|
||||
let loadStart = performance.now();
|
||||
@@ -37,10 +63,11 @@ export class SceneMgr {
|
||||
// Remove the model from the document
|
||||
document.value = await removeModel(name, document.value)
|
||||
|
||||
// Display the final fully loaded model
|
||||
await this.showCurrentDoc(sceneUrl, document);
|
||||
|
||||
console.log("Model", name, "removed in", performance.now() - loadStart, "ms");
|
||||
|
||||
// Reload the helpers to fit the new model (will also show the document)
|
||||
await this.reloadHelpers(sceneUrl, document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,18 +16,26 @@ import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {mdiCircleOpacity, mdiDelete, mdiRectangle, mdiRectangleOutline, mdiVectorRectangle} from '@mdi/js'
|
||||
import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue';
|
||||
|
||||
const props = defineProps<{ mesh: Mesh, viewer: InstanceType<typeof ModelViewerWrapper> | null, document: Document }>();
|
||||
const props = defineProps<{
|
||||
meshes: Array<Mesh>,
|
||||
viewer: InstanceType<typeof ModelViewerWrapper> | null,
|
||||
document: Document
|
||||
}>();
|
||||
const emit = defineEmits<{ remove: [] }>()
|
||||
|
||||
let modelName = props.mesh.getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||
|
||||
let faceCount = props.mesh.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length
|
||||
let edgeCount = props.mesh.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]).length
|
||||
let vertexCount = props.mesh.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length
|
||||
let faceCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length).reduce((a, b) => a + b, 0)
|
||||
let edgeCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]).length).reduce((a, b) => a + b, 0)
|
||||
let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0)
|
||||
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
const opacity = defineModel<number>("opacity", {default: 1});
|
||||
|
||||
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||
if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||
if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||
|
||||
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
//console.log('Enabled features may have changed', newEnabledFeatures)
|
||||
let scene = props.viewer?.scene;
|
||||
|
||||
@@ -9,8 +9,18 @@ import Model from "./Model.vue";
|
||||
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null, document: Document }>();
|
||||
const emit = defineEmits<{ remove: [string] }>()
|
||||
|
||||
function meshList(document: Document) {
|
||||
return document.getRoot().listMeshes();
|
||||
function meshesList(document: Document): Array<Array<Mesh>> {
|
||||
// Grouped by shared name
|
||||
return document.getRoot().listMeshes().reduce((acc, mesh) => {
|
||||
let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed';
|
||||
let group = acc.find((group) => meshName(group[0]) === name);
|
||||
if (group) {
|
||||
group.push(mesh);
|
||||
} else {
|
||||
acc.push([mesh]);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<Array<Mesh>>);
|
||||
}
|
||||
|
||||
function meshName(mesh: Mesh) {
|
||||
@@ -24,8 +34,8 @@ function onRemove(mesh: Mesh) {
|
||||
|
||||
<template>
|
||||
<Loading v-if="!props.document"/>
|
||||
<v-expansion-panels v-else v-for="mesh in meshList(props.document)" :key="meshName(mesh)">
|
||||
<model :mesh="mesh" :viewer="props.viewer" :document="props.document" @remove="onRemove(mesh)"/>
|
||||
<v-expansion-panels v-else v-for="meshes in meshesList(props.document)" :key="meshName(meshes[0])">
|
||||
<model :meshes="meshes" :viewer="props.viewer" :document="props.document" @remove="onRemove(meshes[0])"/>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user