mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-25 17:04:27 +01:00
frontend complete migration from parcel to vite for much better production builds
This commit is contained in:
13
frontend/misc/Loading.vue
Normal file
13
frontend/misc/Loading.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components/index.mjs";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row justify="center" style="height: 100%">
|
||||
<v-col align-self="center">
|
||||
<v-progress-circular indeterminate style="display: block; margin: 0 auto;"/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
54
frontend/misc/Sidebar.vue
Normal file
54
frontend/misc/Sidebar.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
|
||||
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
|
||||
const props = defineProps<{
|
||||
openedInit: Boolean,
|
||||
side: "left" | "right",
|
||||
width: number
|
||||
}>();
|
||||
|
||||
let opened = ref(props.openedInit.valueOf());
|
||||
const openIcon = props.side === 'left' ? mdiChevronRight : mdiChevronLeft;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn icon @click="opened = !opened" class="open-button" :class="side">
|
||||
<svg-icon type="mdi" :path="openIcon"/>
|
||||
</v-btn>
|
||||
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width">
|
||||
<v-toolbar density="compact">
|
||||
<v-toolbar-items v-if="side == 'right'">
|
||||
<slot name="toolbar-items"></slot>
|
||||
<v-btn icon @click="opened = !opened">
|
||||
<svg-icon type="mdi" :path="mdiClose"/>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
<slot name="toolbar"></slot>
|
||||
<v-toolbar-items v-if="side == 'left'">
|
||||
<slot name="toolbar-items"></slot>
|
||||
<v-btn icon @click="opened = !opened">
|
||||
<svg-icon type="mdi" :path="mdiClose"/>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<slot/>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<style scoped>
|
||||
.open-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
/*z-index: 1;*/
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.open-button.right {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
77
frontend/misc/distances.ts
Normal file
77
frontend/misc/distances.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
|
||||
import type {MObject3D} from "../tools/Selection.vue";
|
||||
import type { ModelScene } from '@google/model-viewer/lib/three-components/ModelScene';
|
||||
|
||||
|
||||
function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
|
||||
center: Vector3,
|
||||
vertices: Array<Vector3>
|
||||
} {
|
||||
obj.updateMatrixWorld();
|
||||
let pos: BufferAttribute | InterleavedBufferAttribute = obj.geometry.getAttribute('position');
|
||||
let ind: BufferAttribute | null = obj.geometry.index;
|
||||
if (!ind) {
|
||||
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 = 0; i < ind.count; i++) {
|
||||
let index = ind.array[i];
|
||||
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
|
||||
vertex = scene.target.worldToLocal(obj.localToWorld(vertex));
|
||||
center.add(vertex);
|
||||
vertices.push(vertex);
|
||||
}
|
||||
center = center.divideScalar(ind.count);
|
||||
console.log("center", center)
|
||||
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: MObject3D, b: MObject3D, 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);
|
||||
|
||||
// Find the closest and farthest vertices.
|
||||
// TODO: Compute actual min and max distances between the two objects.
|
||||
// FIXME: Working for points and lines, but not triangles...
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the results.
|
||||
return {
|
||||
min: minDistanceVertices,
|
||||
center: [aCenter, bCenter],
|
||||
max: maxDistanceVertices
|
||||
};
|
||||
}
|
||||
82
frontend/misc/gltf.ts
Normal file
82
frontend/misc/gltf.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {Document, Scene, type Transform, WebIO, Buffer} from "@gltf-transform/core";
|
||||
import {unpartition} from "@gltf-transform/functions";
|
||||
|
||||
let io = new WebIO();
|
||||
export let extrasNameKey = "__yacv_name";
|
||||
export let extrasNameValueHelpers = "__helpers";
|
||||
|
||||
/**
|
||||
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
|
||||
*
|
||||
* It can replace previous models in the document if the provided name matches the name of a previous model.
|
||||
*
|
||||
* Remember to call mergeFinalize after all models have been merged (slower required operations).
|
||||
*/
|
||||
export async function mergePartial(url: string, name: string, document: Document): Promise<Document> {
|
||||
// Load the new document
|
||||
let newDoc = await io.read(url);
|
||||
|
||||
// Remove any previous model with the same name
|
||||
await document.transform(dropByName(name));
|
||||
|
||||
// Ensure consistent names
|
||||
// noinspection TypeScriptValidateJSTypes
|
||||
await newDoc.transform(setNames(name));
|
||||
|
||||
// Merge the new document into the current one
|
||||
return document.merge(newDoc);
|
||||
}
|
||||
|
||||
export async function mergeFinalize(document: Document): Promise<Document> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
export async function removeModel(name: string, document: Document): Promise<Document> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
118
frontend/misc/helpers.ts
Normal file
118
frontend/misc/helpers.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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]]
|
||||
}
|
||||
|
||||
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('VEC3')
|
||||
.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)
|
||||
}
|
||||
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];
|
||||
let rawColors = [
|
||||
[...(AxesColors.x[0]), ...(AxesColors.x[1])],
|
||||
[...(AxesColors.y[0]), ...(AxesColors.y[1])],
|
||||
[...(AxesColors.z[0]), ...(AxesColors.z[1])],
|
||||
].map(g => g.map(x => x / 255.0));
|
||||
buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes');
|
||||
buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes');
|
||||
buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes');
|
||||
buildSimpleGltf(doc, [0, 0, 0], [0], null, transform, '__helper_axes', WebGL2RenderingContext.POINTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Grid helper as a GLTF model, useful for debugging sizes with an OrthographicCamera.
|
||||
*
|
||||
* The grid is built as a box of triangles (representing lines) looking to the inside of the box.
|
||||
* This ensures that only the back of the grid is always visible, regardless of the camera position.
|
||||
*/
|
||||
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4 = new Matrix4(), divisions = 10) {
|
||||
// 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.002) {
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
|
||||
}
|
||||
65
frontend/misc/network.ts
Normal file
65
frontend/misc/network.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {settings} from "./settings";
|
||||
|
||||
export class NetworkUpdateEvent extends Event {
|
||||
name: string;
|
||||
url: string;
|
||||
|
||||
constructor(name: string, url: string) {
|
||||
super("update");
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/** Listens for updates and emits events when a model changes */
|
||||
export class NetworkManager extends EventTarget {
|
||||
private knownObjectHashes: { [name: string]: string | null } = {};
|
||||
|
||||
/**
|
||||
* Tries to load a new model (.glb) from the given URL.
|
||||
*
|
||||
* If the URL uses the websocket protocol (ws:// or wss://), the server will be continuously monitored for changes.
|
||||
* In this case, it will only trigger updates if the name or hash of any model changes.
|
||||
*
|
||||
* Updates will be emitted as "update" events, including the download URL and the model name.
|
||||
*/
|
||||
async load(url: string) {
|
||||
if (url.startsWith("ws://") || url.startsWith("wss://")) {
|
||||
this.monitorWebSocket(url);
|
||||
} else {
|
||||
// Get the last part of the URL as the "name" of the model
|
||||
let name = url.split("/").pop();
|
||||
name = name?.split(".")[0] || `unknown-${Math.random()}`;
|
||||
// Use a head request to get the hash of the file
|
||||
let response = await fetch(url, {method: "HEAD"});
|
||||
let hash = response.headers.get("etag");
|
||||
// Only trigger an update if the hash has changed
|
||||
this.foundModel(name, hash, url);
|
||||
}
|
||||
}
|
||||
|
||||
private monitorWebSocket(url: string) {
|
||||
// WARNING: This will spam the console logs with failed requests when the server is down
|
||||
let ws = new WebSocket(url);
|
||||
ws.onmessage = (event) => {
|
||||
let data = JSON.parse(event.data);
|
||||
console.debug("WebSocket message", data);
|
||||
let urlObj = new URL(url);
|
||||
urlObj.protocol = urlObj.protocol === "ws:" ? "http:" : "https:";
|
||||
urlObj.searchParams.set("api_object", data.name);
|
||||
this.foundModel(data.name, data.hash, urlObj.toString());
|
||||
};
|
||||
ws.onerror = () => ws.close();
|
||||
ws.onclose = () => setTimeout(() => this.monitorWebSocket(url), settings.monitorEveryMs);
|
||||
let timeoutFaster = setTimeout(() => ws.close(), settings.monitorOpenTimeoutMs);
|
||||
ws.onopen = () => clearTimeout(timeoutFaster);
|
||||
}
|
||||
|
||||
private foundModel(name: string, hash: string | null, url: string) {
|
||||
let prevHash = this.knownObjectHashes[name];
|
||||
if (!hash || hash !== prevHash) {
|
||||
this.knownObjectHashes[name] = hash;
|
||||
this.dispatchEvent(new NetworkUpdateEvent(name, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
97
frontend/misc/scene.ts
Normal file
97
frontend/misc/scene.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {type Ref} from 'vue';
|
||||
import {Document} from '@gltf-transform/core';
|
||||
import {extrasNameKey, extrasNameValueHelpers, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
||||
import {newAxes, newGridBox} from "./helpers";
|
||||
import {Vector3} from "three/src/math/Vector3.js"
|
||||
import {Box3} from "three/src/math/Box3.js"
|
||||
import {Matrix4} from "three/src/math/Matrix4.js"
|
||||
|
||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||
export class SceneMgr {
|
||||
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
|
||||
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string): Promise<Document> {
|
||||
let loadStart = performance.now();
|
||||
|
||||
// Start merging into the current document, replacing or adding as needed
|
||||
document = await mergePartial(url, name, document);
|
||||
|
||||
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
|
||||
|
||||
if (name !== extrasNameValueHelpers) {
|
||||
// Reload the helpers to fit the new model
|
||||
// TODO: Only reload the helpers after a few milliseconds of no more models being added/removed
|
||||
document = await this.reloadHelpers(sceneUrl, document);
|
||||
} else {
|
||||
// Display the final fully loaded model
|
||||
let displayStart = performance.now();
|
||||
document = await this.showCurrentDoc(sceneUrl, document);
|
||||
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document): Promise<Document> {
|
||||
let bb = SceneMgr.getBoundingBox(document);
|
||||
|
||||
// Create the helper axes and grid box
|
||||
let helpersDoc = new Document();
|
||||
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
|
||||
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
|
||||
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
|
||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
||||
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl);
|
||||
}
|
||||
|
||||
static getBoundingBox(document: Document): Box3 {
|
||||
// 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.getRoot().listNodes().forEach(node => {
|
||||
if ((node.getExtras()[extrasNameKey] ?? extrasNameValueHelpers) === extrasNameValueHelpers) 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));
|
||||
return new Box3().setFromCenterAndSize(bbCenter, bbSize);
|
||||
}
|
||||
|
||||
/** Removes a model from the viewer */
|
||||
static async removeModel(sceneUrl: Ref<string>, document: Document, name: string): Promise<Document> {
|
||||
let loadStart = performance.now();
|
||||
|
||||
// Remove the model from the document
|
||||
document = await removeModel(name, document)
|
||||
|
||||
console.log("Model", name, "removed in", performance.now() - loadStart, "ms");
|
||||
|
||||
// Reload the helpers to fit the new model (will also show the document)
|
||||
document = await this.reloadHelpers(sceneUrl, document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/** Serializes the current document into a GLB and updates the viewerSrc */
|
||||
private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
|
||||
// Make sure the document is fully loaded and ready to be shown
|
||||
document = await mergeFinalize(document);
|
||||
|
||||
// Serialize the document into a GLB and update the viewerSrc
|
||||
let buffer = await toBuffer(document);
|
||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||
console.debug("Showing current doc", document, "as", Array.from(buffer));
|
||||
sceneUrl.value = URL.createObjectURL(blob);
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
58
frontend/misc/settings.ts
Normal file
58
frontend/misc/settings.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// These are the default values for the settings, which are overridden below
|
||||
export const settings = {
|
||||
preloadModels: [
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/fox.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
"ws://127.0.0.1:32323/"
|
||||
],
|
||||
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
|
||||
monitorEveryMs: 100,
|
||||
monitorOpenTimeoutMs: 10000,
|
||||
// ModelViewer settings
|
||||
autoplay: true,
|
||||
arModes: 'webxr scene-viewer quick-look',
|
||||
exposure: 1,
|
||||
shadowIntensity: 0,
|
||||
background: '',
|
||||
}
|
||||
|
||||
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
|
||||
function parseSetting(name: string, value: string): any {
|
||||
let arrayElem = name.endsWith(".0")
|
||||
if (arrayElem) name = name.slice(0, -2);
|
||||
let prevValue = (settings as any)[name];
|
||||
if (prevValue === undefined) throw new Error(`Unknown setting: ${name}`);
|
||||
if (Array.isArray(prevValue) && !arrayElem) {
|
||||
let toExtend = []
|
||||
if (!firstTimeNames.includes(name)) {
|
||||
firstTimeNames.push(name);
|
||||
} else {
|
||||
toExtend = prevValue;
|
||||
}
|
||||
toExtend.push(parseSetting(name + ".0", value));
|
||||
return toExtend;
|
||||
}
|
||||
switch (typeof prevValue) {
|
||||
case 'boolean':
|
||||
return value === 'true';
|
||||
case 'number':
|
||||
return Number(value);
|
||||
case 'string':
|
||||
return value;
|
||||
default:
|
||||
throw new Error(`Unknown setting type: ${typeof prevValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-override any settings from the URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key in settings) (settings as any)[key] = parseSetting(key, value);
|
||||
})
|
||||
Reference in New Issue
Block a user