mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Compare commits
79 Commits
v0.10.6
...
yeicor-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed2431f7da | ||
|
|
c4d47afe69 | ||
|
|
ec91ab90f2 | ||
|
|
3f573c75cc | ||
|
|
4eee767afb | ||
|
|
3f9ec31799 | ||
|
|
54a22b1499 | ||
|
|
438d315b53 | ||
|
|
0cc6d81e9e | ||
|
|
2468aeed08 | ||
|
|
a8ec19e2d4 | ||
|
|
741a658b01 | ||
|
|
19663354cc | ||
|
|
e5f2246ff1 | ||
|
|
4cb8a65582 | ||
|
|
fc737583fd | ||
|
|
37ddf9ab63 | ||
|
|
5977172096 | ||
|
|
888239d898 | ||
|
|
fdfc5cdcad | ||
|
|
e2707ac8db | ||
|
|
f0a55efff1 | ||
|
|
71e95ec393 | ||
|
|
454a7249cd | ||
|
|
1501f8c3b5 | ||
|
|
32d34d9ec4 | ||
|
|
9b3b679631 | ||
|
|
dd389f34a3 | ||
|
|
21cdaf2c33 | ||
|
|
ace4d5aa5e | ||
|
|
289677a4ac | ||
|
|
a97c2807d8 | ||
|
|
629aa939ca | ||
|
|
e21fa7d680 | ||
|
|
3933d4842b | ||
|
|
95ffb769e2 | ||
|
|
16627a3cd4 | ||
|
|
0e1f6b7aa0 | ||
|
|
0ff8776a12 | ||
|
|
64e9e26da1 | ||
|
|
7eaba92057 | ||
|
|
6a0aa265b6 | ||
|
|
fbf2f3e9a1 | ||
|
|
74491fc867 | ||
|
|
cc0b087cf7 | ||
|
|
fcacf8172d | ||
|
|
0c93a7e05d | ||
|
|
e50e3c7f09 | ||
|
|
9f76df8668 | ||
|
|
6c7f7382f6 | ||
|
|
c7aee20aac | ||
|
|
8598f7f75e | ||
|
|
7a8ff04cc7 | ||
|
|
ac8bf8cb06 | ||
|
|
7a425e3ccc | ||
|
|
ff3c3d6f2f | ||
|
|
8792ab7139 | ||
|
|
291e3c726d | ||
|
|
be400fde29 | ||
|
|
32fb3230bb | ||
|
|
a63175dc3a | ||
|
|
3c013de8ba | ||
|
|
a659b0561f | ||
|
|
ccd48bcae3 | ||
|
|
1a9cac08cd | ||
|
|
3fb07300b8 | ||
|
|
938c554919 | ||
|
|
5308ececae | ||
|
|
64e2622954 | ||
|
|
2e8b8bd65f | ||
|
|
854de26695 | ||
|
|
e0503983f1 | ||
|
|
021cfd89a1 | ||
|
|
9d96effb30 | ||
|
|
dff2d36b44 | ||
|
|
810647276b | ||
|
|
a54b91f704 | ||
|
|
099726035a | ||
|
|
c383b8573a |
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
name: "Build frontend"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- uses: "actions/setup-node@v4"
|
||||
- uses: "actions/setup-node@v5"
|
||||
with:
|
||||
cache: "yarn"
|
||||
- run: "yarn install"
|
||||
@@ -32,11 +32,11 @@ jobs:
|
||||
name: "Build backend"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
- uses: "actions/setup-python@v6"
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
@@ -48,11 +48,11 @@ jobs:
|
||||
name: "Build logo"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
- uses: "actions/setup-python@v6"
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
@@ -69,11 +69,11 @@ jobs:
|
||||
name: "Build example"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
- uses: "actions/setup-python@v6"
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
|
||||
6
.github/workflows/deploy1.yml
vendored
6
.github/workflows/deploy1.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
outputs:
|
||||
skip: "${{ steps.check_recursive.outputs.skip || 'false' }}" # Default to false if not set
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with: # Ensure we are not in a detached HEAD state
|
||||
ref: "master"
|
||||
token: "${{ secrets.GH_PAT }}"
|
||||
@@ -31,11 +31,11 @@ jobs:
|
||||
fi
|
||||
- run: "echo 'CLEAN_VERSION=${{ github.ref }}' | sed 's,refs/tags/v,,g' >> $GITHUB_ENV"
|
||||
# Write the new version to package.json
|
||||
- uses: "actions/setup-node@v4"
|
||||
- uses: "actions/setup-node@v5"
|
||||
- run: "yarn version --new-version $CLEAN_VERSION --no-git-tag-version"
|
||||
# Write the new version to pyproject.toml
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
- uses: "actions/setup-python@v6"
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
|
||||
10
.github/workflows/deploy2.yml
vendored
10
.github/workflows/deploy2.yml
vendored
@@ -32,12 +32,12 @@ jobs:
|
||||
name: "github-pages"
|
||||
url: "${{ steps.deployment.outputs.page_url }}"
|
||||
steps:
|
||||
- uses: "actions/download-artifact@v4"
|
||||
- uses: "actions/download-artifact@v5"
|
||||
with: # Downloads all artifacts from the build job
|
||||
path: "./public"
|
||||
merge-multiple: true
|
||||
- uses: "actions/configure-pages@v5"
|
||||
- uses: "actions/upload-pages-artifact@v3"
|
||||
- uses: "actions/upload-pages-artifact@v4"
|
||||
with:
|
||||
path: 'public'
|
||||
- id: "deployment"
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
needs: "rebuild"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v5"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- uses: "actions/setup-node@v4"
|
||||
- uses: "actions/setup-node@v5"
|
||||
with:
|
||||
cache: "yarn"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
- uses: "actions/setup-python@v6"
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@
|
||||
|
||||
*.iml
|
||||
venv/
|
||||
__pycache__/
|
||||
|
||||
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Contributing to Yet Another CAD Viewer
|
||||
|
||||
Thank you for your interest in contributing to this project!
|
||||
|
||||
## How to contribute
|
||||
|
||||
- For **non-trivial changes** or new features, please [open an issue](https://github.com/yeicor-3d/yet-another-cad-viewer/issues) first to discuss your idea before submitting a pull request.
|
||||
- For **small fixes** (such as typo corrections or minor improvements), feel free to submit a pull request directly.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Make sure your changes are clear and concise.
|
||||
- Describe what your pull request does and reference any related issues.
|
||||
|
||||
We appreciate your help in improving this project!
|
||||
11
README.md
11
README.md
@@ -10,22 +10,22 @@ in a web browser.
|
||||
- All [GLTF 2.0](https://www.khronos.org/gltf/) features (textures, PBR materials, animations...).
|
||||
- All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
|
||||
- Load multiple models at once, load external models and even images as quads.
|
||||
- Control clipping planes and transparency of each model.
|
||||
- Control clipping planes, transparency, edge/vertex sizes and explode each model.
|
||||
- View and interact with topological entities: faces, edges, vertices and locations.
|
||||
- Select any entity and measure bounding box size and distances.
|
||||
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
||||
- Fully-featured static deployment: just upload the viewer and models to your server.
|
||||
- Build123d playground! Code and build your model fully inside the
|
||||
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code_url=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
|
||||
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
|
||||
|
||||
## Usage
|
||||
|
||||
The [example](example) is a fully working project that shows how to use the viewer.
|
||||
|
||||
You can play with the latest
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)
|
||||
(or
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)).
|
||||
|
||||

|
||||
|
||||
@@ -36,3 +36,6 @@ demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb
|
||||
Uses the same backend and frontend behind the scenes.
|
||||
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
|
||||
Yet Another CAD Viewer and other projects, with automatic updates.
|
||||
- [OCP.wasm](https://github.com/yeicor/OCP.wasm/) ports OCP (OpenCASCADE for Python) and supporting libraries to
|
||||
WebAssembly, enabling full in-browser CAD model generation and manipulation. This powers the build123d playground
|
||||
provided by this viewer.
|
||||
|
||||
@@ -1011,7 +1011,7 @@ third-party archives.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- typescript@5.8.3
|
||||
- typescript@5.9.2
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -1824,7 +1824,7 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- pyodide@0.28.0
|
||||
- pyodide@0.28.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -2038,7 +2038,7 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- vuetify@3.9.3
|
||||
- vuetify@3.9.4
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
for (let modelIndex in event.models) {
|
||||
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
||||
let model = event.models[modelIndex];
|
||||
if (!model) continue;
|
||||
tools.value?.removeObjectSelections(model.name);
|
||||
try {
|
||||
let loadHelpers = (await settings).loadHelpers;
|
||||
@@ -109,6 +110,12 @@ async function loadModelManual() {
|
||||
if (modelUrl) await networkMgr.load(modelUrl);
|
||||
}
|
||||
|
||||
function loadDemoModels() {
|
||||
for (let name of ['fox.glb', 'img.glb', 'location.glb', 'logo.glb', 'logo_hl.glb', 'logo_hl_tex.glb']) {
|
||||
networkMgr.load(`https://yeicor-3d.github.io/yet-another-cad-viewer/${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect dropped .glb files and load them manually
|
||||
document.body.addEventListener("dragover", e => {
|
||||
e.preventDefault(); // Allow drop
|
||||
@@ -124,7 +131,6 @@ document.body.addEventListener("drop", async e => {
|
||||
await networkMgr.load(file);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -139,9 +145,8 @@ document.body.addEventListener("drop", async e => {
|
||||
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiScriptTextPlay" type="mdi"/> Open playground...
|
||||
</v-btn>
|
||||
<v-btn @click="networkMgr.load('https://yeicor-3d.github.io/yet-another-cad-viewer/logo.glb')"
|
||||
class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo model...
|
||||
<v-btn @click="loadDemoModels" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo models...
|
||||
</v-btn>
|
||||
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiPlus" type="mdi"/> Load model manually...
|
||||
@@ -149,7 +154,9 @@ document.body.addEventListener("drop", async e => {
|
||||
<span v-if="preloadingModels.length > 0" class="d-block text-center my-16">
|
||||
<span class="d-block text-center text-h6">Still trying to load the following:</span>
|
||||
<span class="d-block text-center" v-for="(model, index) in preloadingModels" :key="index">
|
||||
{{ model }}<span v-if="index < preloadingModels.length - 1">, </span>
|
||||
<template v-if="model !== undefined">
|
||||
{{ model }}<span v-if="index < preloadingModels.length - 1">, </span>
|
||||
</template>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -186,4 +193,4 @@ html, body {
|
||||
height: 100%;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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,112 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,61 +116,61 @@ 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];
|
||||
}
|
||||
|
||||
@@ -56,8 +56,13 @@ export class NetworkManager extends EventTarget {
|
||||
name = name.replace('.glb', '').replace('.gltf', '');
|
||||
} else {
|
||||
// If there is a #name parameter in the URL, use it as the name
|
||||
let urlObj = new URL(url);
|
||||
let hashParams = new URLSearchParams(urlObj.hash.slice(1));
|
||||
let hashParams: URLSearchParams
|
||||
try {
|
||||
let urlObj = new URL(url, window.location.href);
|
||||
hashParams = new URLSearchParams(urlObj.hash.slice(1));
|
||||
} catch (e) {
|
||||
hashParams = new URLSearchParams();
|
||||
}
|
||||
if (hashParams.has("name")) {
|
||||
name = hashParams.get("name") || `unknown-${Math.random()}`;
|
||||
} else { // Default to the last part of the URL as the "name" of the model
|
||||
|
||||
@@ -99,7 +99,7 @@ export class SceneMgr {
|
||||
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)]));
|
||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc) as ArrayBufferView<ArrayBuffer>]));
|
||||
let newDocument = await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
|
||||
URL.revokeObjectURL(helpersUrl);
|
||||
return newDocument;
|
||||
@@ -112,7 +112,7 @@ export class SceneMgr {
|
||||
|
||||
// Serialize the document into a GLB and update the viewerSrc
|
||||
let buffer = await toBuffer(document);
|
||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||
let blob = new Blob([buffer as ArrayBufferView<ArrayBuffer>], {type: 'model/gltf-binary'});
|
||||
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||
if (sceneUrl.value.startsWith("blob:")) URL.revokeObjectURL(sceneUrl.value);
|
||||
sceneUrl.value = URL.createObjectURL(blob);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const settings = (async () => {
|
||||
// @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,
|
||||
// new URL('../../assets/logo_build/img.glb', import.meta.url).href,
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
'<auto>', // Get the default preload URL if not overridden
|
||||
],
|
||||
@@ -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
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
} from "vuetify/lib/components/index.mjs";
|
||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||
import {Mesh} from "@gltf-transform/core";
|
||||
import {ref, watch} from "vue";
|
||||
import {nextTick, ref, watch} from "vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {
|
||||
mdiArrowExpand,
|
||||
mdiCircleOpacity,
|
||||
mdiCube,
|
||||
mdiDelete,
|
||||
@@ -44,7 +45,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
const emit = defineEmits<{ remove: [] }>()
|
||||
|
||||
let modelName = props.meshes[0].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"
|
||||
|
||||
// Count the number of faces, edges and vertices
|
||||
let faceCount = ref(-1);
|
||||
@@ -58,7 +59,12 @@ const clipPlaneY = ref(1);
|
||||
const clipPlaneSwappedY = ref(false);
|
||||
const clipPlaneZ = ref(1);
|
||||
const clipPlaneSwappedZ = ref(false);
|
||||
|
||||
const edgeWidth = ref(0);
|
||||
const explodeStrength = ref(0);
|
||||
const explodeSwapped = ref(false);
|
||||
|
||||
// Load the settings for the default edge width
|
||||
(async () => {
|
||||
let s = await settings;
|
||||
edgeWidth.value = s.edgeWidth;
|
||||
@@ -163,9 +169,9 @@ function onClipPlanesChange() {
|
||||
new Plane(new Vector3(0, -1, 0), offsetY).applyMatrix4(rotSceneMatrix),
|
||||
new Plane(new Vector3(0, 0, 1), -offsetZ).applyMatrix4(rotSceneMatrix),
|
||||
];
|
||||
if (clipPlaneSwappedX.value) planes[0].negate();
|
||||
if (clipPlaneSwappedY.value) planes[1].negate();
|
||||
if (clipPlaneSwappedZ.value) planes[2].negate();
|
||||
if (clipPlaneSwappedX.value) planes[0]?.negate();
|
||||
if (clipPlaneSwappedY.value) planes[1]?.negate();
|
||||
if (clipPlaneSwappedZ.value) planes[2]?.negate();
|
||||
if (!enabledZ) planes.pop();
|
||||
if (!enabledY) planes.splice(1, 1);
|
||||
if (!enabledX) planes.shift();
|
||||
@@ -246,6 +252,76 @@ function onEdgeWidthChange(newEdgeWidth: number) {
|
||||
|
||||
watch(edgeWidth, onEdgeWidthChange);
|
||||
|
||||
// Explode the model
|
||||
function onExplodeChange(newExplodeStrength: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
|
||||
// Get direction and size of the explosion in a first pass
|
||||
const meBbox = new Box3();
|
||||
const othersBbox = new Box3();
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child == sceneModel) return; // Skip the scene itself
|
||||
const isMe = child.userData[extrasNameKey] === modelName;
|
||||
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
|
||||
child.type === 'Line' || child.type === 'LineSegments' ||
|
||||
child.type === 'Points') && !child.userData.noHit) {
|
||||
if (isMe) {
|
||||
meBbox.expandByObject(child);
|
||||
} else if (!isMe && child.userData[extrasNameKey]) {
|
||||
othersBbox.expandByObject(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
const modelSize = new Vector3();
|
||||
meBbox.getSize(modelSize);
|
||||
const maxDimension = Math.max(modelSize.x, modelSize.y, modelSize.z);
|
||||
const pushDirection = new Vector3().subVectors(meBbox.getCenter(new Vector3()), othersBbox.getCenter(new Vector3())).normalize();
|
||||
|
||||
|
||||
// Use absolute value for strength calculation
|
||||
let strength = Math.abs(newExplodeStrength);
|
||||
if (explodeSwapped.value) strength = -strength;
|
||||
|
||||
// Apply explosion
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
|
||||
child.type === 'Line' || child.type === 'LineSegments' ||
|
||||
child.type === 'Points')) {
|
||||
|
||||
// Handle zero vector case (if object is at origin)
|
||||
const direction = pushDirection.clone();
|
||||
if (direction.lengthSq() < 0.0001) {
|
||||
direction.set(0, 1, 0);
|
||||
console.warn("Explode direction was zero, using (0, 1, 0) instead");
|
||||
}
|
||||
|
||||
// Calculate new position based on model size
|
||||
const factor = strength * maxDimension;
|
||||
const newPosition = new Vector3().add(direction.multiplyScalar(factor));
|
||||
|
||||
// Apply new position
|
||||
child.position.copy(newPosition);
|
||||
|
||||
// Update related objects (back is automatically updated)
|
||||
if (child.userData.niceLine) {
|
||||
child.userData.niceLine.position.copy(newPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scene.queueRender();
|
||||
onClipPlanesChange();
|
||||
}
|
||||
|
||||
// Add watchers for explode variables
|
||||
watch(explodeStrength, (newVal) => onExplodeChange(newVal));
|
||||
watch(explodeSwapped, () => onExplodeChange(explodeStrength.value));
|
||||
|
||||
|
||||
function onModelLoad() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
@@ -312,15 +388,17 @@ function onModelLoad() {
|
||||
|
||||
// Furthermore...
|
||||
// Enabled features may have been reset after a reload
|
||||
onEnabledFeaturesChange(enabledFeatures.value)
|
||||
onEnabledFeaturesChange(enabledFeatures.value);
|
||||
// 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)
|
||||
onWireframeChange(wireframe.value);
|
||||
// Clip planes may have been reset after a reload
|
||||
onClipPlanesChange()
|
||||
onClipPlanesChange();
|
||||
// Edge width may have been reset after a reload
|
||||
onEdgeWidthChange(edgeWidth.value)
|
||||
onEdgeWidthChange(edgeWidth.value);
|
||||
// Explode may have been reset after a reload
|
||||
if (explodeStrength.value > 0) nextTick(() => onExplodeChange(explodeStrength.value));
|
||||
|
||||
scene.queueRender()
|
||||
}
|
||||
@@ -370,6 +448,21 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-model="explodeStrength" hide-details max="1" min="0">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Explode model</v-tooltip>
|
||||
<svg-icon :path="mdiArrowExpand" type="mdi"></svg-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-tooltip activator="parent">Swap explode direction (may go crazy)</v-tooltip>
|
||||
<v-checkbox-btn v-model="explodeSwapped" falseIcon="mdi-checkbox-blank-outline"
|
||||
trueIcon="mdi-checkbox-marked-outline">
|
||||
<template v-slot:label>
|
||||
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
||||
@@ -482,4 +575,4 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
.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>
|
||||
|
||||
@@ -17,7 +17,7 @@ function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
|
||||
// Grouped by shared name
|
||||
return sceneDocument.getRoot().listMeshes().reduce((acc, mesh) => {
|
||||
let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed';
|
||||
let group = acc.find((group) => meshName(group[0]) === name);
|
||||
let group = acc.find((group) => group[0] && meshName(group[0]) === name);
|
||||
if (group) {
|
||||
group.push(mesh);
|
||||
} else {
|
||||
@@ -43,9 +43,9 @@ defineExpose({findModel})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
|
||||
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshes[0] ? meshName(meshes[0]) : 'unnamed'"
|
||||
v-model="expandedNames as any" multiple>
|
||||
<model :meshes="meshes" :viewer="props.viewer" @remove="onRemove(meshes[0])"/>
|
||||
<model :meshes="meshes" :viewer="props.viewer" @remove="meshes[0] ? onRemove(meshes[0]) : undefined"/>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ function onModelData(modelData: string) {
|
||||
// - Save for upload and share link feature
|
||||
builtModelsGlb[modelMetadata.name] = binaryData;
|
||||
// - Create a Blob from the binary data to be used as a URL
|
||||
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
|
||||
const blob = new Blob([binaryData as ArrayBufferView<ArrayBuffer>], {type: 'model/gltf-binary'});
|
||||
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
|
||||
} else {
|
||||
delete builtModelsGlb[modelMetadata.name]; // Remove from built models if it's a remove request
|
||||
@@ -165,13 +165,21 @@ function resetWorker(loadSnapshot: Uint8Array | undefined = undefined) {
|
||||
setupPyodide(false, loadSnapshot); // Reinitialize Pyodide
|
||||
}
|
||||
|
||||
function shareLinkCommon(added: Record<string, string>, forgotten: Array<string>) {
|
||||
function shareLinkCommon(added: Record<string, Array<string> | string>, forgotten: Array<string>) {
|
||||
const baseUrl = window.location
|
||||
const searchParams = new URLSearchParams(baseUrl.search);
|
||||
for (const k of forgotten) searchParams.delete(k);
|
||||
const hashParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
|
||||
for (const k of forgotten) hashParams.delete(k);
|
||||
for (const k in added) hashParams.append(k, added[k]); // Prefer hash to GET
|
||||
for (const k in added) {
|
||||
if (Array.isArray(added[k])) {
|
||||
for (const v of added[k]) {
|
||||
hashParams.append(k, v); // Prefer hash to GET
|
||||
}
|
||||
} else if (typeof added[k] === 'string') {
|
||||
hashParams.set(k, added[k]); // Prefer hash to GET
|
||||
}
|
||||
}
|
||||
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`;
|
||||
output(`Share link ready: ${shareUrl}\n`)
|
||||
if (navigator.clipboard?.writeText === undefined) {
|
||||
@@ -195,14 +203,16 @@ async function uploadAndShareLink() {
|
||||
|
||||
// Upload code.py
|
||||
const codeBlob = new Blob([model.value.code], {type: 'text/x-python'});
|
||||
const newParams: Record<string, string> = {
|
||||
const newParams: Record<string, string | Array<string>> = {
|
||||
'pg_code': await uploadFile('code.py', new Uint8Array(await codeBlob.arrayBuffer()))
|
||||
};
|
||||
|
||||
// Upload all models
|
||||
newParams['preload'] = []
|
||||
for (const name in builtModelsGlb) {
|
||||
const glb: any = builtModelsGlb[name];
|
||||
newParams['preload'] = await uploadFile(name + '.glb', glb);
|
||||
const url = await uploadFile(name + '.glb', glb)
|
||||
newParams['preload'].push(url); // Add to preload list
|
||||
}
|
||||
|
||||
// Build share URL
|
||||
|
||||
@@ -213,7 +213,7 @@ function toggleSelection() {
|
||||
function toggleOpenNextSelection() {
|
||||
openNextSelection.value = [
|
||||
!openNextSelection.value[0],
|
||||
openNextSelection.value[0] ? openNextSelection.value[1] : selectionEnabled.value
|
||||
openNextSelection.value[0] ? (openNextSelection.value[1] ?? false) : selectionEnabled.value
|
||||
];
|
||||
if (openNextSelection.value[0]) {
|
||||
// Reuse selection code to identify the model
|
||||
@@ -330,14 +330,20 @@ function updateBoundingBox() {
|
||||
// Only draw one edge per axis, the 2nd closest one to the camera
|
||||
for (let edgeI in edgesByAxis) {
|
||||
let axisEdges = edgesByAxis[edgeI];
|
||||
let edge: Array<number> = axisEdges[0];
|
||||
if (!axisEdges || axisEdges.length === 0) continue;
|
||||
let edge: Array<number> = axisEdges[0] ?? [];
|
||||
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
|
||||
edge = axisEdges[0];
|
||||
if (!axisEdges || axisEdges.length === 0) break;
|
||||
edge = axisEdges[0] ?? [];
|
||||
let edgeDist = Infinity;
|
||||
let cameraPos: Vector3 = props.viewer?.scene?.camera?.position ?? new Vector3();
|
||||
for (let testEdge of axisEdges) {
|
||||
let from = new Vector3(...corners[testEdge[0]]);
|
||||
let to = new Vector3(...corners[testEdge[1]]);
|
||||
if (!testEdge || testEdge.length < 2) continue;
|
||||
let cornerA = corners[testEdge[0] ?? 0];
|
||||
let cornerB = corners[testEdge[1] ?? 0];
|
||||
if (!cornerA || !cornerB) continue;
|
||||
let from = new Vector3(...cornerA);
|
||||
let to = new Vector3(...cornerB);
|
||||
let mid = from.clone().add(to).multiplyScalar(0.5);
|
||||
let newDist = cameraPos.distanceTo(mid);
|
||||
if (newDist < edgeDist) {
|
||||
@@ -347,11 +353,16 @@ function updateBoundingBox() {
|
||||
}
|
||||
axisEdges = axisEdges.filter((e) => e !== edge);
|
||||
}
|
||||
let from = new Vector3(...corners[edge[0]]);
|
||||
let to = new Vector3(...corners[edge[1]]);
|
||||
if (!edge || edge.length < 2) continue;
|
||||
let cornerA = corners[edge[0] ?? 0];
|
||||
let cornerB = corners[edge[1] ?? 0];
|
||||
if (!cornerA || !cornerB) continue;
|
||||
let from = new Vector3(...cornerA);
|
||||
let to = new Vector3(...cornerB);
|
||||
let length = to.clone().sub(from).length();
|
||||
if (length < 0.05) continue; // Skip very small edges (e.g. a single point)
|
||||
let color = [AxesColors.x, AxesColors.y, AxesColors.z][edgeI][1]; // Secondary colors
|
||||
let colorArray = [AxesColors.x, AxesColors.y, AxesColors.z][parseInt(edgeI)];
|
||||
let color = colorArray ? colorArray[1] : [255, 255, 255]; // Secondary colors
|
||||
let lineCacheKey = JSON.stringify([from, to]);
|
||||
let matchingLine = boundingBoxLines[lineCacheKey];
|
||||
if (matchingLine) {
|
||||
@@ -359,7 +370,7 @@ function updateBoundingBox() {
|
||||
} else {
|
||||
let newLineId = props.viewer?.addLine3D(from, to,
|
||||
length.toFixed(1) + "mm", {
|
||||
"stroke": "rgb(" + color.join(',') + ")",
|
||||
"stroke": "rgb(" + (color ?? [255, 255, 255]).join(',') + ")",
|
||||
"stroke-width": "2"
|
||||
});
|
||||
if (newLineId) {
|
||||
@@ -410,10 +421,17 @@ function updateDistances() {
|
||||
}
|
||||
|
||||
// Add lines (if not already added)
|
||||
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
|
||||
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(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
|
||||
if (!selected.value[0] || !selected.value[1] || !props.viewer?.scene) return;
|
||||
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer.scene);
|
||||
if (max[0] && max[1]) {
|
||||
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
|
||||
}
|
||||
if (center[0] && center[1]) {
|
||||
ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
|
||||
}
|
||||
if (min[0] && min[1]) {
|
||||
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
|
||||
}
|
||||
|
||||
// Remove the lines that are no longer needed
|
||||
for (let lineLocator of distanceLinesToRemove) {
|
||||
@@ -504,4 +522,4 @@ window.addEventListener('keydown', (event) => {
|
||||
top: -12px;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -12,8 +12,8 @@ import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"
|
||||
//@ts-ignore
|
||||
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
(self as any).MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
if (label === "json") {
|
||||
return new jsonWorker()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {loadPyodide, type PyodideInterface} from "pyodide";
|
||||
import {loadPyodide, type PyodideAPI} from "pyodide";
|
||||
|
||||
let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide({
|
||||
...initOpts,
|
||||
@@ -10,7 +10,7 @@ let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide
|
||||
},
|
||||
});
|
||||
|
||||
let pyodideReadyPromise: Promise<PyodideInterface> | null = null;
|
||||
let pyodideReadyPromise: Promise<PyodideAPI> | null = null;
|
||||
|
||||
export type MessageEventDataIn = {
|
||||
type: 'asyncRun';
|
||||
@@ -41,7 +41,7 @@ self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
||||
// Create a directory tree in the Pyodide filesystem.
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
try {
|
||||
pyodide.FS.mkdirTree(event.data.path);
|
||||
await pyodide.FS.mkdirTree(event.data.path);
|
||||
self.postMessage({id: event.data.id, result: true});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
@@ -51,7 +51,7 @@ self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
||||
// Write a file to the Pyodide filesystem.
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
try {
|
||||
pyodide.FS.writeFile(event.data.path, event.data.content);
|
||||
await pyodide.FS.writeFile(event.data.path, event.data.content);
|
||||
self.postMessage({id: event.data.id, result: true});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
|
||||
@@ -1,163 +1,182 @@
|
||||
// Model management from the graphics side
|
||||
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import type {Intersection} from "three";
|
||||
import {Box3} from "three";
|
||||
import {extrasNameKey} from "../misc/gltf";
|
||||
import type { MObject3D } from "./Selection.vue";
|
||||
import type { Intersection } from "three";
|
||||
import { Box3 } from "three";
|
||||
import { extrasNameKey } from "../misc/gltf";
|
||||
|
||||
/** 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]
|
||||
/** 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;
|
||||
}
|
||||
constructor(object: MObject3D, kind: "face" | "edge" | "vertex", indices: [number, number]) {
|
||||
this.object = object;
|
||||
this.kind = kind;
|
||||
this.indices = indices;
|
||||
}
|
||||
|
||||
public getObjectName() {
|
||||
return this.object.userData[extrasNameKey];
|
||||
}
|
||||
public getObjectName() {
|
||||
return this.object.userData[extrasNameKey];
|
||||
}
|
||||
|
||||
public matches(object: MObject3D) {
|
||||
return this.getObjectName() === object.userData[extrasNameKey] &&
|
||||
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
|
||||
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
|
||||
this.kind === 'vertex' && object.type === 'Points')
|
||||
}
|
||||
public matches(object: MObject3D) {
|
||||
return (
|
||||
this.getObjectName() === object.userData[extrasNameKey] &&
|
||||
((this.kind === "face" && (object.type === "Mesh" || object.type === "SkinnedMesh")) ||
|
||||
(this.kind === "edge" && (object.type === "Line" || object.type === "LineSegments")) ||
|
||||
(this.kind === "vertex" && object.type === "Points"))
|
||||
);
|
||||
}
|
||||
|
||||
public getKey() {
|
||||
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
||||
}
|
||||
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]);
|
||||
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] ?? Infinity, x);
|
||||
min[1] = Math.min(min[1] ?? Infinity, y);
|
||||
min[2] = Math.min(min[2] ?? Infinity, z);
|
||||
max[0] = Math.max(max[0] ?? -Infinity, x);
|
||||
max[1] = Math.max(max[1] ?? -Infinity, y);
|
||||
max[2] = Math.max(max[2] ?? -Infinity, 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;
|
||||
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) 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]];
|
||||
}
|
||||
}
|
||||
let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
|
||||
if (!hit.faceIndex) 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 ?? 0];
|
||||
} 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;
|
||||
}
|
||||
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]];
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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]);
|
||||
}
|
||||
return prevColor!;
|
||||
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]);
|
||||
// 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()];
|
||||
}
|
||||
// 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()];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import encryptCode from "tanmayo7lock/dist/index.js?raw";
|
||||
//@ts-expect-error
|
||||
import encryptCode from "tanmayo7lock?raw";
|
||||
|
||||
function encrypt(msg: string, secret: string = "hudfhgd8fghdfgh3uhuifdgh"): string {
|
||||
let exports: any = {};
|
||||
@@ -52,7 +53,7 @@ export async function uploadFile(name: string, data: Uint8Array): Promise<string
|
||||
// Upload file to the locker
|
||||
const uploadUrl = `https://vouz-backend.onrender.com/api/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob([data], {type: 'application/octet-stream'}), name);
|
||||
formData.append('file', new Blob([data as ArrayBufferView<ArrayBuffer>], {type: 'application/octet-stream'}), name);
|
||||
formData.append("name", encrypt(lockerName));
|
||||
formData.append("passkey", encrypt(lockerName));
|
||||
const response = await fetch(uploadUrl, {
|
||||
|
||||
@@ -161,9 +161,9 @@ function addLine3D(p1: Vector3, p2: Vector3, centerText?: string, lineAttrs: { [
|
||||
function removeLine3D(id: number): boolean {
|
||||
if (!scene.value || !(id in lines.value)) return false;
|
||||
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'}));
|
||||
lines.value[id].startHotspot.parentElement?.remove()
|
||||
lines.value[id]?.startHotspot.parentElement?.remove()
|
||||
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'}));
|
||||
lines.value[id].endHotspot.parentElement?.remove()
|
||||
lines.value[id]?.endHotspot.parentElement?.remove()
|
||||
delete lines.value[id];
|
||||
scene.value.queueRender() // Needed to update the hotspots
|
||||
return true;
|
||||
@@ -175,17 +175,17 @@ function onCameraChangeLine(lineId: number) {
|
||||
if (!(lineId in lines.value) || !(elem.value)) return // Silently ignore (not updated yet)
|
||||
// Update start and end 2D positions
|
||||
let {x: xB, y: yB} = elem.value.getBoundingClientRect();
|
||||
let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect();
|
||||
lines.value[lineId].start2D = [x - xB, y - yB];
|
||||
let {x: x2, y: y2} = lines.value[lineId].endHotspot.getBoundingClientRect();
|
||||
lines.value[lineId].end2D = [x2 - xB, y2 - yB];
|
||||
let {x, y} = lines.value[lineId]?.startHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
|
||||
if (lines.value[lineId]) lines.value[lineId].start2D = [x - xB, y - yB];
|
||||
let {x: x2, y: y2} = lines.value[lineId]?.endHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
|
||||
if (lines.value[lineId]) lines.value[lineId].end2D = [x2 - xB, y2 - yB];
|
||||
|
||||
// Update the center text size if needed
|
||||
if (svg.value && lines.value[lineId].centerText && lines.value[lineId].centerTextSize[0] === 0) {
|
||||
if (svg.value && lines.value[lineId]?.centerText && lines.value[lineId]?.centerTextSize[0] === 0) {
|
||||
let text = svg.value.getElementsByClassName('line' + lineId + '_text')[0] as SVGTextElement | undefined;
|
||||
if (text) {
|
||||
let bbox = text.getBBox();
|
||||
lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
|
||||
if (lines.value[lineId]) lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,96 @@
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {$scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||
import {settings} from "../misc/settings.ts";
|
||||
import { ModelViewerElement } from "@google/model-viewer";
|
||||
import { $scene } from "@google/model-viewer/lib/model-viewer-base";
|
||||
import { settings } from "../misc/settings.ts";
|
||||
|
||||
export let currentSceneRotation = 0; // radians, 0 is the default rotation
|
||||
|
||||
export async function setupLighting(modelViewer: ModelViewerElement) {
|
||||
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
|
||||
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
|
||||
let lastX: number;
|
||||
let panning = false;
|
||||
let radiansPerPixel: number;
|
||||
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
|
||||
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
|
||||
let lastX: number;
|
||||
let panning = false;
|
||||
let radiansPerPixel: number;
|
||||
|
||||
const startPan = () => {
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
const {radius} = orbit;
|
||||
radiansPerPixel = -1 * radius / modelViewer.getBoundingClientRect().height;
|
||||
modelViewer.interactionPrompt = 'none';
|
||||
};
|
||||
const startPan = () => {
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
const { radius } = orbit;
|
||||
radiansPerPixel = (-1 * radius) / modelViewer.getBoundingClientRect().height;
|
||||
modelViewer.interactionPrompt = "none";
|
||||
};
|
||||
|
||||
const updatePan = (thisX: number) => {
|
||||
const delta = (thisX - lastX) * radiansPerPixel;
|
||||
lastX = thisX;
|
||||
currentSceneRotation += delta;
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
orbit.theta += delta;
|
||||
modelViewer.cameraOrbit = orbit.toString();
|
||||
modelViewer.resetTurntableRotation(currentSceneRotation);
|
||||
modelViewer.jumpCameraToGoal();
|
||||
}
|
||||
const updatePan = (thisX: number) => {
|
||||
const delta = (thisX - lastX) * radiansPerPixel;
|
||||
lastX = thisX;
|
||||
currentSceneRotation += delta;
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
orbit.theta += delta;
|
||||
modelViewer.cameraOrbit = orbit.toString();
|
||||
modelViewer.resetTurntableRotation(currentSceneRotation);
|
||||
modelViewer.jumpCameraToGoal();
|
||||
};
|
||||
|
||||
modelViewer.addEventListener('mousedown', (event) => {
|
||||
panning = event.metaKey || event.shiftKey;
|
||||
if (!panning)
|
||||
return;
|
||||
modelViewer.addEventListener(
|
||||
"mousedown",
|
||||
(event) => {
|
||||
panning = event.metaKey || event.shiftKey;
|
||||
if (!panning) return;
|
||||
|
||||
lastX = event.clientX;
|
||||
startPan();
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
lastX = event.clientX;
|
||||
startPan();
|
||||
event.stopPropagation();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
modelViewer.addEventListener('touchstart', (event) => {
|
||||
const {targetTouches, touches} = event;
|
||||
panning = targetTouches.length === 2 && targetTouches.length === touches.length;
|
||||
if (!panning)
|
||||
return;
|
||||
modelViewer.addEventListener(
|
||||
"touchstart",
|
||||
(event) => {
|
||||
const { targetTouches, touches } = event;
|
||||
panning = targetTouches.length === 2 && targetTouches.length === touches.length;
|
||||
if (!panning) return;
|
||||
|
||||
lastX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
|
||||
startPan();
|
||||
}, true);
|
||||
lastX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
|
||||
startPan();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
if (!panning)
|
||||
return;
|
||||
document.addEventListener(
|
||||
"mousemove",
|
||||
(event) => {
|
||||
if (!panning) return;
|
||||
|
||||
updatePan(event.clientX);
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
updatePan(event.clientX);
|
||||
event.stopPropagation();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
modelViewer.addEventListener('touchmove', (event) => {
|
||||
if (!panning || event.targetTouches.length !== 2)
|
||||
return;
|
||||
modelViewer.addEventListener(
|
||||
"touchmove",
|
||||
(event) => {
|
||||
if (!panning || event.targetTouches.length !== 2) return;
|
||||
|
||||
const {targetTouches} = event;
|
||||
const thisX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
|
||||
updatePan(thisX);
|
||||
}, true);
|
||||
const { targetTouches } = event;
|
||||
const thisX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
|
||||
updatePan(thisX);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
document.addEventListener('mouseup', (event) => {
|
||||
panning = false;
|
||||
}, true);
|
||||
document.addEventListener(
|
||||
"mouseup",
|
||||
(event) => {
|
||||
panning = false;
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
modelViewer.addEventListener('touchend', (event) => {
|
||||
panning = false;
|
||||
}, true);
|
||||
}
|
||||
modelViewer.addEventListener(
|
||||
"touchend",
|
||||
(event) => {
|
||||
panning = false;
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yet-another-cad-viewer",
|
||||
"version": "0.10.6",
|
||||
"version": "0.10.11",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -24,11 +24,11 @@
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/svg": "^7.4.47",
|
||||
"js-base64": "^3.7.7",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"pako": "^2.1.0",
|
||||
"pyodide": "^0.28.0",
|
||||
"tanmayo7lock": "^1.0.18",
|
||||
"three": "^0.179.0",
|
||||
"three": "^0.180.0",
|
||||
"three-mesh-bvh": "^0.9.0",
|
||||
"three-orientation-gizmo": "git+https://github.com/jrj2211/three-orientation-gizmo.git",
|
||||
"vue": "^3.5.13",
|
||||
@@ -38,16 +38,16 @@
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^22.9.3",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/three": "^0.178.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"@vue/tsconfig": "^0.8.0",
|
||||
"buffer": "^5.5.0||^6.0.0",
|
||||
"commander": "^14.0.0",
|
||||
"generate-license-file": "^4.0.0",
|
||||
"npm-run-all2": "^8.0.0",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "~5.8.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-static-copy": "^3.1.1",
|
||||
"vue-tsc": "^3.0.0"
|
||||
|
||||
856
poetry.lock
generated
856
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "yacv-server"
|
||||
version = "0.10.6"
|
||||
version = "0.10.11"
|
||||
description = "Yet Another CAD Viewer (server)"
|
||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
@@ -28,7 +28,7 @@ build123d = ">=0.9,<0.10"
|
||||
# Misc
|
||||
pygltflib = "^1.16.2"
|
||||
pillow = ">=10.2,<12.0"
|
||||
poetry-core = "==2.1.3"
|
||||
poetry-core = "==2.2.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
taskipy = "^1.14.1"
|
||||
|
||||
@@ -117,7 +117,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
# Handle arguments
|
||||
if name is None:
|
||||
if isinstance(source, str):
|
||||
name = os.path.basename(source)
|
||||
name, _ = os.path.splitext(os.path.basename(source))
|
||||
else:
|
||||
hasher = hashlib.md5()
|
||||
hasher.update(source)
|
||||
|
||||
@@ -42,8 +42,8 @@ class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
path = super().translate_path(path)
|
||||
path = os.path.realpath(path) # Avoid symlink hacks
|
||||
if self.directory: # Ensure proper subdirectory
|
||||
base = os.path.abspath(self.directory)
|
||||
if not os.path.abspath(path).startswith(base):
|
||||
base = os.path.realpath(self.directory)
|
||||
if not path.startswith(base):
|
||||
self.send_error(HTTPStatus.FORBIDDEN, "Path is not in the frontend directory")
|
||||
return ''
|
||||
return path
|
||||
|
||||
Reference in New Issue
Block a user