mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-21 15:04:25 +01:00
chore(deps): update dependency @vue/tsconfig to ^0.8.0 (#251)
* chore(deps): update dependency @vue/tsconfig to ^0.8.0 * Fix new ts issues * Add null checks for selection and model objects throughout frontend This improves robustness by handling cases where selection or model objects may be missing or undefined, preventing runtime errors. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Yeicor <4929005+yeicor@users.noreply.github.com>
This commit is contained in:
@@ -1,75 +1,88 @@
|
||||
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
|
||||
import type {MObject3D} from "../tools/Selection.vue";
|
||||
import type {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene';
|
||||
import type {SelectionInfo} from "../tools/selection";
|
||||
import { BufferAttribute, InterleavedBufferAttribute, Vector3 } from "three";
|
||||
import type { MObject3D } from "../tools/Selection.vue";
|
||||
import type { ModelScene } from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
import type { SelectionInfo } from "../tools/selection";
|
||||
|
||||
|
||||
function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
|
||||
center: Vector3,
|
||||
vertices: Array<Vector3>
|
||||
function getCenterAndVertexList(
|
||||
selInfo: SelectionInfo,
|
||||
scene: ModelScene,
|
||||
): {
|
||||
center: Vector3;
|
||||
vertices: Array<Vector3>;
|
||||
} {
|
||||
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
||||
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
||||
if (ind === null) {
|
||||
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
ind.array[i] = i;
|
||||
}
|
||||
if (!selInfo.object?.geometry) {
|
||||
throw new Error("selInfo.object or geometry is undefined");
|
||||
}
|
||||
let pos = selInfo.object.geometry.getAttribute("position");
|
||||
let ind = selInfo.object.geometry.index;
|
||||
if (ind === null) {
|
||||
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
ind.array[i] = i;
|
||||
}
|
||||
let center = new Vector3();
|
||||
let vertices = [];
|
||||
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
|
||||
let index = ind.getX(i)
|
||||
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
|
||||
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
|
||||
center.add(vertex);
|
||||
vertices.push(vertex);
|
||||
}
|
||||
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
|
||||
return {center, vertices};
|
||||
}
|
||||
let center = new Vector3();
|
||||
let vertices = [];
|
||||
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
|
||||
let index = ind.getX(i);
|
||||
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
|
||||
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
|
||||
center.add(vertex);
|
||||
vertices.push(vertex);
|
||||
}
|
||||
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
|
||||
return { center, vertices };
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two THREE.Object3D objects, returns their closest and farthest vertices, and the geometric centers.
|
||||
* All of them are approximated and should not be used for precise calculations.
|
||||
*/
|
||||
export function distances(a: SelectionInfo, b: SelectionInfo, scene: ModelScene): {
|
||||
min: Array<Vector3>,
|
||||
center: Array<Vector3>,
|
||||
max: Array<Vector3>
|
||||
export function distances(
|
||||
a: SelectionInfo,
|
||||
b: SelectionInfo,
|
||||
scene: ModelScene,
|
||||
): {
|
||||
min: Array<Vector3>;
|
||||
center: Array<Vector3>;
|
||||
max: Array<Vector3>;
|
||||
} {
|
||||
// Simplify this problem (approximate) by using the distance between each of their vertices.
|
||||
// Find the center of each object.
|
||||
let {center: aCenter, vertices: aVertices} = getCenterAndVertexList(a, scene);
|
||||
let {center: bCenter, vertices: bVertices} = getCenterAndVertexList(b, scene);
|
||||
// Simplify this problem (approximate) by using the distance between each of their vertices.
|
||||
// Find the center of each object.
|
||||
let { center: aCenter, vertices: aVertices } = getCenterAndVertexList(a, scene);
|
||||
let { center: bCenter, vertices: bVertices } = getCenterAndVertexList(b, scene);
|
||||
|
||||
// Find the closest and farthest vertices.
|
||||
// TODO: Compute actual min and max distances between the two objects.
|
||||
// FIXME: Really slow... (use a BVH or something)
|
||||
let minDistance = Infinity;
|
||||
let minDistanceVertices = [new Vector3(), new Vector3()];
|
||||
let maxDistance = -Infinity;
|
||||
let maxDistanceVertices = [new Vector3(), new Vector3()];
|
||||
for (let i = 0; i < aVertices.length; i++) {
|
||||
for (let j = 0; j < bVertices.length; j++) {
|
||||
let distance = aVertices[i].distanceTo(bVertices[j]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
minDistanceVertices[0] = aVertices[i];
|
||||
minDistanceVertices[1] = bVertices[j];
|
||||
}
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance;
|
||||
maxDistanceVertices[0] = aVertices[i];
|
||||
maxDistanceVertices[1] = bVertices[j];
|
||||
}
|
||||
// Find the closest and farthest vertices.
|
||||
// TODO: Compute actual min and max distances between the two objects.
|
||||
// FIXME: Really slow... (use a BVH or something)
|
||||
let minDistance = Infinity;
|
||||
let minDistanceVertices = [new Vector3(), new Vector3()];
|
||||
let maxDistance = -Infinity;
|
||||
let maxDistanceVertices = [new Vector3(), new Vector3()];
|
||||
for (let i = 0; i < aVertices.length; i++) {
|
||||
for (let j = 0; j < bVertices.length; j++) {
|
||||
const aVertex = aVertices[i];
|
||||
const bVertex = bVertices[j];
|
||||
if (aVertex && bVertex) {
|
||||
let distance = aVertex.distanceTo(bVertex);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
minDistanceVertices[0] = aVertex;
|
||||
minDistanceVertices[1] = bVertex;
|
||||
}
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance;
|
||||
maxDistanceVertices[0] = aVertex;
|
||||
maxDistanceVertices[1] = bVertex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the results.
|
||||
return {
|
||||
min: minDistanceVertices,
|
||||
center: [aCenter, bCenter],
|
||||
max: maxDistanceVertices
|
||||
};
|
||||
}
|
||||
// Return the results.
|
||||
return {
|
||||
min: minDistanceVertices,
|
||||
center: [aCenter, bCenter],
|
||||
max: maxDistanceVertices,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
|
||||
import {mergeDocuments, unpartition} from "@gltf-transform/functions";
|
||||
import {retrieveFile} from "../tools/upload-file.ts";
|
||||
import { Buffer, Document, Scene, type Transform, WebIO } from "@gltf-transform/core";
|
||||
import { mergeDocuments, unpartition } from "@gltf-transform/functions";
|
||||
import { retrieveFile } from "../tools/upload-file.ts";
|
||||
|
||||
let io = new WebIO();
|
||||
export let extrasNameKey = "__yacv_name";
|
||||
export let extrasNameValueHelpers = "__helpers";
|
||||
|
||||
// @ts-expect-error
|
||||
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
|
||||
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== "undefined" && __YACV_SMALL_BUILD__;
|
||||
|
||||
/**
|
||||
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
|
||||
@@ -16,133 +16,148 @@ let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_B
|
||||
*
|
||||
* Remember to call mergeFinalize after all models have been merged (slower required operations).
|
||||
*/
|
||||
export async function mergePartial(url: string | Blob, name: string, document: Document, networkFinished: () => void = () => {
|
||||
}): Promise<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 fetchOrRead(url);
|
||||
let buffer = await response.arrayBuffer();
|
||||
networkFinished();
|
||||
export async function mergePartial(
|
||||
url: string | Blob,
|
||||
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 fetchOrRead(url);
|
||||
let buffer = await response.arrayBuffer();
|
||||
networkFinished();
|
||||
|
||||
// Load the new document
|
||||
let newDoc = null;
|
||||
let alreadyTried: { [name: string]: boolean } = {}
|
||||
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
|
||||
try { // Try to load fast if no extensions are used
|
||||
newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||
} catch (e) { // Fallback to wait for download and register big extensions
|
||||
if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
|
||||
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
|
||||
// @ts-expect-error
|
||||
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||
// @ts-expect-error
|
||||
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||
io.registerExtensions([KHRDracoMeshCompression])
|
||||
.registerDependencies({
|
||||
'draco3d.decoder': await dracoDecoderWeb.default({}),
|
||||
'draco3d.encoder': await dracoEncoderWeb.default({})
|
||||
});
|
||||
} else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
|
||||
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
|
||||
io.registerExtensions([EXTTextureWebP]);
|
||||
} else { // TODO: Add more extensions as required
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Load the new document
|
||||
let newDoc = null;
|
||||
let alreadyTried: { [name: string]: boolean } = {};
|
||||
while (newDoc == null) {
|
||||
// Retry adding extensions as required until the document is loaded
|
||||
try {
|
||||
// Try to load fast if no extensions are used
|
||||
newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||
} catch (e) {
|
||||
// Fallback to wait for download and register big extensions
|
||||
if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||
if (alreadyTried["draco"]) throw e;
|
||||
else alreadyTried["draco"] = true;
|
||||
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||
let { KHRDracoMeshCompression } = await import("@gltf-transform/extensions");
|
||||
// @ts-expect-error
|
||||
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||
// @ts-expect-error
|
||||
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||
io.registerExtensions([KHRDracoMeshCompression]).registerDependencies({
|
||||
"draco3d.decoder": await dracoDecoderWeb.default({}),
|
||||
"draco3d.encoder": await dracoEncoderWeb.default({}),
|
||||
});
|
||||
} else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||
if (alreadyTried["webp"]) throw e;
|
||||
else alreadyTried["webp"] = true;
|
||||
let { EXTTextureWebP } = await import("@gltf-transform/extensions");
|
||||
io.registerExtensions([EXTTextureWebP]);
|
||||
} else {
|
||||
// TODO: Add more extensions as required
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any previous model with the same name
|
||||
await document.transform(dropByName(name));
|
||||
// Remove any previous model with the same name
|
||||
await document.transform(dropByName(name));
|
||||
|
||||
// Ensure consistent names
|
||||
// noinspection TypeScriptValidateJSTypes
|
||||
await newDoc.transform(setNames(name));
|
||||
// Ensure consistent names
|
||||
// noinspection TypeScriptValidateJSTypes
|
||||
await newDoc.transform(setNames(name));
|
||||
|
||||
// Merge the new document into the current one
|
||||
mergeDocuments(document, newDoc);
|
||||
return document;
|
||||
// Merge the new document into the current one
|
||||
mergeDocuments(document, newDoc);
|
||||
return document;
|
||||
}
|
||||
|
||||
export async function mergeFinalize(document: Document): Promise<Document> {
|
||||
// Single scene & buffer required before loading & rendering
|
||||
return await document.transform(mergeScenes(), unpartition());
|
||||
// Single scene & buffer required before loading & rendering
|
||||
return await document.transform(mergeScenes(), unpartition());
|
||||
}
|
||||
|
||||
export async function toBuffer(doc: Document): Promise<Uint8Array> {
|
||||
return io.writeBinary(doc);
|
||||
return io.writeBinary(doc);
|
||||
}
|
||||
|
||||
export async function removeModel(name: string, document: Document): Promise<Document> {
|
||||
return await document.transform(dropByName(name));
|
||||
return await document.transform(dropByName(name));
|
||||
}
|
||||
|
||||
/** Given a parsed GLTF document and a name, it forces the names of all elements to be identified by the name (or derivatives) */
|
||||
function setNames(name: string): Transform {
|
||||
return (doc: Document) => {
|
||||
// Do this automatically for all elements changing any name
|
||||
for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
|
||||
if (!elem.getExtras()) elem.setExtras({});
|
||||
elem.getExtras()[extrasNameKey] = name;
|
||||
}
|
||||
return doc;
|
||||
return (doc: Document) => {
|
||||
// Do this automatically for all elements changing any name
|
||||
for (let elem of doc
|
||||
.getGraph()
|
||||
.listEdges()
|
||||
.map((e) => e.getChild())) {
|
||||
if (!elem.getExtras()) elem.setExtras({});
|
||||
elem.getExtras()[extrasNameKey] = name;
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
}
|
||||
|
||||
/** Ensures that all elements with the given name are removed from the document */
|
||||
function dropByName(name: string): Transform {
|
||||
return (doc: Document) => {
|
||||
for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
|
||||
if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
|
||||
if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
|
||||
elem.dispose();
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
return (doc: Document) => {
|
||||
for (let elem of doc
|
||||
.getGraph()
|
||||
.listEdges()
|
||||
.map((e) => e.getChild())) {
|
||||
if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
|
||||
if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
|
||||
elem.dispose();
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Merges all scenes in the document into a single default scene */
|
||||
function mergeScenes(): Transform {
|
||||
return (doc: Document) => {
|
||||
let root = doc.getRoot();
|
||||
let scene = root.getDefaultScene() ?? root.listScenes()[0];
|
||||
for (let dropScene of root.listScenes()) {
|
||||
if (dropScene === scene) continue;
|
||||
for (let node of dropScene.listChildren()) {
|
||||
scene.addChild(node);
|
||||
}
|
||||
dropScene.dispose();
|
||||
}
|
||||
return (doc: Document) => {
|
||||
let root = doc.getRoot();
|
||||
let scene = root.getDefaultScene() ?? root.listScenes()[0];
|
||||
if (!scene) {
|
||||
throw new Error("No scene found in GLTF document");
|
||||
}
|
||||
for (let dropScene of root.listScenes()) {
|
||||
if (dropScene === scene) continue;
|
||||
for (let node of dropScene.listChildren()) {
|
||||
scene.addChild(node);
|
||||
}
|
||||
dropScene.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Fetches a URL or reads it if it is a Blob URL */
|
||||
async function fetchOrRead(url: string | Blob) {
|
||||
if (url instanceof Blob) {
|
||||
// Use the FileReader API as fetch does not support Blob URLs
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = (event: ProgressEvent<FileReader>) => {
|
||||
if (event.target && event.target.result) {
|
||||
resolve(new Response(event.target.result));
|
||||
} else {
|
||||
reject(new Error("Failed to read Blob URL: " + url));
|
||||
}
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
reject(new Error("Error reading Blob URL: " + url + " - " + error));
|
||||
};
|
||||
// Read the Blob URL as an ArrayBuffer
|
||||
reader.readAsArrayBuffer(new Blob([url]));
|
||||
});
|
||||
} else {
|
||||
// Fetch the URL
|
||||
return retrieveFile(url);
|
||||
}
|
||||
|
||||
if (url instanceof Blob) {
|
||||
// Use the FileReader API as fetch does not support Blob URLs
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = (event: ProgressEvent<FileReader>) => {
|
||||
if (event.target && event.target.result) {
|
||||
resolve(new Response(event.target.result));
|
||||
} else {
|
||||
reject(new Error("Failed to read Blob URL: " + url));
|
||||
}
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
reject(new Error("Error reading Blob URL: " + url + " - " + error));
|
||||
};
|
||||
// Read the Blob URL as an ArrayBuffer
|
||||
reader.readAsArrayBuffer(new Blob([url]));
|
||||
});
|
||||
} else {
|
||||
// Fetch the URL
|
||||
return retrieveFile(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +1,125 @@
|
||||
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
|
||||
|
||||
import {Document, type TypedArray} from '@gltf-transform/core'
|
||||
import {Vector2} from "three/src/math/Vector2.js"
|
||||
import {Vector3} from "three/src/math/Vector3.js"
|
||||
import {Matrix4} from "three/src/math/Matrix4.js"
|
||||
|
||||
import { Document, type TypedArray } from "@gltf-transform/core";
|
||||
import { Vector2 } from "three/src/math/Vector2.js";
|
||||
import { Vector3 } from "three/src/math/Vector3.js";
|
||||
import { Matrix4 } from "three/src/math/Matrix4.js";
|
||||
|
||||
/** Exports the colors used for the axes, primary and secondary. They match the orientation gizmo. */
|
||||
export const AxesColors = {
|
||||
x: [[247, 60, 60], [148, 36, 36]],
|
||||
z: [[108, 203, 38], [65, 122, 23]],
|
||||
y: [[23, 140, 240], [14, 84, 144]]
|
||||
}
|
||||
x: [
|
||||
[247, 60, 60],
|
||||
[148, 36, 36],
|
||||
],
|
||||
z: [
|
||||
[108, 203, 38],
|
||||
[65, 122, 23],
|
||||
],
|
||||
y: [
|
||||
[23, 140, 240],
|
||||
[14, 84, 144],
|
||||
],
|
||||
};
|
||||
|
||||
function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: number[], rawColors: number[] | null, transform: Matrix4, name: string = '__helper', mode: number = WebGL2RenderingContext.LINES) {
|
||||
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + 'Buffer')
|
||||
const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + 'Scene')
|
||||
const positions = doc.createAccessor(name + 'Position')
|
||||
.setArray(new Float32Array(rawPositions) as TypedArray)
|
||||
.setType('VEC3')
|
||||
.setBuffer(buffer)
|
||||
const indices = doc.createAccessor(name + 'Indices')
|
||||
.setArray(new Uint32Array(rawIndices) as TypedArray)
|
||||
.setType('SCALAR')
|
||||
.setBuffer(buffer)
|
||||
let colors = null;
|
||||
if (rawColors) {
|
||||
colors = doc.createAccessor(name + 'Color')
|
||||
.setArray(new Float32Array(rawColors) as TypedArray)
|
||||
.setType('VEC4')
|
||||
.setBuffer(buffer);
|
||||
}
|
||||
const material = doc.createMaterial(name + 'Material')
|
||||
.setAlphaMode('OPAQUE')
|
||||
const geometry = doc.createPrimitive()
|
||||
.setIndices(indices)
|
||||
.setAttribute('POSITION', positions)
|
||||
.setMode(mode as any)
|
||||
.setMaterial(material)
|
||||
if (rawColors) {
|
||||
geometry.setAttribute('COLOR_0', colors)
|
||||
}
|
||||
if (mode == WebGL2RenderingContext.TRIANGLES) {
|
||||
geometry.setExtras({face_triangles_end: [rawIndices.length / 6, rawIndices.length * 2 / 6, rawIndices.length * 3 / 6, rawIndices.length * 4 / 6, rawIndices.length * 5 / 6, rawIndices.length]})
|
||||
} else if (mode == WebGL2RenderingContext.LINES) {
|
||||
geometry.setExtras({edge_points_end: [rawIndices.length / 3, rawIndices.length * 2 / 3, rawIndices.length]})
|
||||
}
|
||||
const mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
|
||||
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
|
||||
scene.addChild(node)
|
||||
function buildSimpleGltf(
|
||||
doc: Document,
|
||||
rawPositions: number[],
|
||||
rawIndices: number[],
|
||||
rawColors: number[] | null,
|
||||
transform: Matrix4,
|
||||
name: string = "__helper",
|
||||
mode: number = WebGL2RenderingContext.LINES,
|
||||
) {
|
||||
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + "Buffer");
|
||||
const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + "Scene");
|
||||
if (!scene) throw new Error("Scene is undefined");
|
||||
if (!rawPositions) throw new Error("rawPositions is undefined");
|
||||
const positions = doc
|
||||
.createAccessor(name + "Position")
|
||||
.setArray(new Float32Array(rawPositions) as TypedArray)
|
||||
.setType("VEC3")
|
||||
.setBuffer(buffer);
|
||||
const indices = doc
|
||||
.createAccessor(name + "Indices")
|
||||
.setArray(new Uint32Array(rawIndices) as TypedArray)
|
||||
.setType("SCALAR")
|
||||
.setBuffer(buffer);
|
||||
let colors = null;
|
||||
if (rawColors) {
|
||||
colors = doc
|
||||
.createAccessor(name + "Color")
|
||||
.setArray(new Float32Array(rawColors) as TypedArray)
|
||||
.setType("VEC4")
|
||||
.setBuffer(buffer);
|
||||
}
|
||||
const material = doc.createMaterial(name + "Material").setAlphaMode("OPAQUE");
|
||||
const geometry = doc
|
||||
.createPrimitive()
|
||||
.setIndices(indices)
|
||||
.setAttribute("POSITION", positions)
|
||||
.setMode(mode as any)
|
||||
.setMaterial(material);
|
||||
if (rawColors) {
|
||||
geometry.setAttribute("COLOR_0", colors);
|
||||
}
|
||||
if (mode == WebGL2RenderingContext.TRIANGLES) {
|
||||
geometry.setExtras({
|
||||
face_triangles_end: [
|
||||
rawIndices.length / 6,
|
||||
(rawIndices.length * 2) / 6,
|
||||
(rawIndices.length * 3) / 6,
|
||||
(rawIndices.length * 4) / 6,
|
||||
(rawIndices.length * 5) / 6,
|
||||
rawIndices.length,
|
||||
],
|
||||
});
|
||||
} else if (mode == WebGL2RenderingContext.LINES) {
|
||||
geometry.setExtras({ edge_points_end: [rawIndices.length / 3, (rawIndices.length * 2) / 3, rawIndices.length] });
|
||||
}
|
||||
const mesh = doc.createMesh(name + "Mesh").addPrimitive(geometry);
|
||||
const node = doc
|
||||
.createNode(name + "Node")
|
||||
.setMesh(mesh)
|
||||
.setMatrix(transform.elements as any);
|
||||
scene.addChild(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
|
||||
*/
|
||||
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
let rawIndices = [0, 1, 2, 3, 4, 5];
|
||||
let rawPositions = [
|
||||
0, 0, 0, size.x, 0, 0,
|
||||
0, 0, 0, 0, size.y, 0,
|
||||
0, 0, 0, 0, 0, -size.z,
|
||||
];
|
||||
let rawColors = [
|
||||
...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
|
||||
...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
|
||||
...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
|
||||
].map(x => x / 255.0);
|
||||
// Axes at (0, 0, 0)
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
|
||||
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
|
||||
// Axes at center
|
||||
if (new Matrix4() != transform) {
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes_center');
|
||||
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes_center', WebGL2RenderingContext.POINTS);
|
||||
}
|
||||
let rawIndices = [0, 1, 2, 3, 4, 5];
|
||||
let rawPositions = [0, 0, 0, size.x, 0, 0, 0, 0, 0, 0, size.y, 0, 0, 0, 0, 0, 0, -size.z];
|
||||
let rawColors = [
|
||||
...(AxesColors.x[0] ?? [255, 0, 0]),
|
||||
255,
|
||||
...(AxesColors.x[1] ?? [255, 0, 0]),
|
||||
255,
|
||||
...(AxesColors.y[0] ?? [0, 255, 0]),
|
||||
255,
|
||||
...(AxesColors.y[1] ?? [0, 255, 0]),
|
||||
255,
|
||||
...(AxesColors.z[0] ?? [0, 0, 255]),
|
||||
255,
|
||||
...(AxesColors.z[1] ?? [0, 0, 255]),
|
||||
255,
|
||||
].map((x) => x / 255.0);
|
||||
// Axes at (0, 0, 0)
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), "__helper_axes");
|
||||
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), "__helper_axes", WebGL2RenderingContext.POINTS);
|
||||
// Axes at center
|
||||
if (new Matrix4() != transform) {
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, "__helper_axes_center");
|
||||
buildSimpleGltf(
|
||||
doc,
|
||||
[0, 0, 0],
|
||||
[0],
|
||||
[1, 1, 1, 1],
|
||||
transform,
|
||||
"__helper_axes_center",
|
||||
WebGL2RenderingContext.POINTS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,61 +129,69 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
* This ensures that only the back of the grid is always visible, regardless of the camera position.
|
||||
*/
|
||||
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
|
||||
// Create transformed positions for the inner faces of the box
|
||||
let allPositions: number[] = [];
|
||||
let allIndices: number[] = [];
|
||||
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
|
||||
for (let positive of [1, -1]) {
|
||||
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
|
||||
let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z)
|
||||
let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0))
|
||||
let size2 = new Vector2();
|
||||
if (axis.x) size2.set(size.z, size.y);
|
||||
if (axis.y) size2.set(size.x, size.z);
|
||||
if (axis.z) size2.set(size.x, size.y);
|
||||
let transform = new Matrix4().multiply(translation).multiply(rotation);
|
||||
let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
|
||||
let baseIndex = allPositions.length / 3;
|
||||
for (let i of rawIndices) {
|
||||
allIndices.push(i + baseIndex);
|
||||
}
|
||||
// Apply transform to the positions before adding them to the list
|
||||
for (let i = 0; i < rawPositions.length; i += 3) {
|
||||
let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
|
||||
pos.applyMatrix4(transform);
|
||||
allPositions.push(pos.x, pos.y, pos.z);
|
||||
}
|
||||
}
|
||||
// Create transformed positions for the inner faces of the box
|
||||
let allPositions: number[] = [];
|
||||
let allIndices: number[] = [];
|
||||
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
|
||||
for (let positive of [1, -1]) {
|
||||
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
|
||||
let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
|
||||
let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0));
|
||||
let size2 = new Vector2();
|
||||
if (axis.x) size2.set(size.z, size.y);
|
||||
if (axis.y) size2.set(size.x, size.z);
|
||||
if (axis.z) size2.set(size.x, size.y);
|
||||
let transform = new Matrix4().multiply(translation).multiply(rotation);
|
||||
let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
|
||||
let baseIndex = allPositions.length / 3;
|
||||
for (let i of rawIndices) {
|
||||
allIndices.push(i + baseIndex);
|
||||
}
|
||||
// Apply transform to the positions before adding them to the list
|
||||
for (let i = 0; i < rawPositions.length; i += 3) {
|
||||
let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
|
||||
pos.applyMatrix4(transform);
|
||||
allPositions.push(pos.x, pos.y, pos.z);
|
||||
}
|
||||
}
|
||||
let colors = new Array(allPositions.length / 3 * 4).fill(1);
|
||||
buildSimpleGltf(doc, allPositions, allIndices, colors, baseTransform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
|
||||
}
|
||||
let colors = new Array((allPositions.length / 3) * 4).fill(1);
|
||||
buildSimpleGltf(
|
||||
doc,
|
||||
allPositions,
|
||||
allIndices,
|
||||
colors,
|
||||
baseTransform,
|
||||
"__helper_grid",
|
||||
WebGL2RenderingContext.TRIANGLES,
|
||||
);
|
||||
}
|
||||
|
||||
export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] {
|
||||
const rawPositions = [];
|
||||
const rawIndices = [];
|
||||
// Build the grid as triangles
|
||||
for (let i = 0; i <= divisions; i++) {
|
||||
const x = -size.x / 2 + size.x * i / divisions;
|
||||
const y = -size.y / 2 + size.y * i / divisions;
|
||||
const rawPositions = [];
|
||||
const rawIndices = [];
|
||||
// Build the grid as triangles
|
||||
for (let i = 0; i <= divisions; i++) {
|
||||
const x = -size.x / 2 + (size.x * i) / divisions;
|
||||
const y = -size.y / 2 + (size.y * i) / divisions;
|
||||
|
||||
// Vertical quad (two triangles)
|
||||
rawPositions.push(x - divisionWidth * size.x / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + divisionWidth * size.x / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + divisionWidth * size.x / 2, size.y / 2, 0);
|
||||
rawPositions.push(x - divisionWidth * size.x / 2, size.y / 2, 0);
|
||||
const baseIndex = i * 4;
|
||||
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
||||
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
||||
// Vertical quad (two triangles)
|
||||
rawPositions.push(x - (divisionWidth * size.x) / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + (divisionWidth * size.x) / 2, -size.y / 2, 0);
|
||||
rawPositions.push(x + (divisionWidth * size.x) / 2, size.y / 2, 0);
|
||||
rawPositions.push(x - (divisionWidth * size.x) / 2, size.y / 2, 0);
|
||||
const baseIndex = i * 4;
|
||||
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
||||
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
||||
|
||||
// Horizontal quad (two triangles)
|
||||
rawPositions.push(-size.x / 2, y - divisionWidth * size.y / 2, 0);
|
||||
rawPositions.push(size.x / 2, y - divisionWidth * size.y / 2, 0);
|
||||
rawPositions.push(size.x / 2, y + divisionWidth * size.y / 2, 0);
|
||||
rawPositions.push(-size.x / 2, y + divisionWidth * size.y / 2, 0);
|
||||
const baseIndex2 = (divisions + 1 + i) * 4;
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||
}
|
||||
return [rawPositions, rawIndices];
|
||||
}
|
||||
// Horizontal quad (two triangles)
|
||||
rawPositions.push(-size.x / 2, y - (divisionWidth * size.y) / 2, 0);
|
||||
rawPositions.push(size.x / 2, y - (divisionWidth * size.y) / 2, 0);
|
||||
rawPositions.push(size.x / 2, y + (divisionWidth * size.y) / 2, 0);
|
||||
rawPositions.push(-size.x / 2, y + (divisionWidth * size.y) / 2, 0);
|
||||
const baseIndex2 = (divisions + 1 + i) * 4;
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||
}
|
||||
return [rawPositions, rawIndices];
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const settings = (async () => {
|
||||
url = "dev+http://localhost:32323";
|
||||
}
|
||||
}
|
||||
settings.preload[i] = url;
|
||||
settings.preload[i] = url ?? "";
|
||||
}
|
||||
|
||||
// Auto-decompress the code and other playground settings
|
||||
|
||||
Reference in New Issue
Block a user