diff --git a/src/misc/helpers.ts b/src/misc/helpers.ts index ab75ecb..82c5b73 100644 --- a/src/misc/helpers.ts +++ b/src/misc/helpers.ts @@ -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); } diff --git a/src/misc/scene.ts b/src/misc/scene.ts index 31fcee2..e5935b7 100644 --- a/src/misc/scene.ts +++ b/src/misc/scene.ts @@ -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, document: ShallowRef) { + // 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, document: ShallowRef, 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; } diff --git a/src/models/Model.vue b/src/models/Model.vue index da31add..626c83d 100644 --- a/src/models/Model.vue +++ b/src/models/Model.vue @@ -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 | null, document: Document }>(); +const props = defineProps<{ + meshes: Array, + viewer: InstanceType | 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>("enabledFeatures", {default: [0, 1, 2]}); const opacity = defineModel("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) { //console.log('Enabled features may have changed', newEnabledFeatures) let scene = props.viewer?.scene; diff --git a/src/models/Models.vue b/src/models/Models.vue index 3e363cc..8a31c29 100644 --- a/src/models/Models.vue +++ b/src/models/Models.vue @@ -9,8 +9,18 @@ import Model from "./Model.vue"; const props = defineProps<{ viewer: InstanceType | null, document: Document }>(); const emit = defineEmits<{ remove: [string] }>() -function meshList(document: Document) { - return document.getRoot().listMeshes(); +function meshesList(document: Document): Array> { + // 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>); } function meshName(mesh: Mesh) { @@ -24,8 +34,8 @@ function onRemove(mesh: Mesh) {