mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ba0e18479 | ||
|
|
eca2bbfa7c | ||
|
|
86180c424e | ||
|
|
e42d6be074 | ||
|
|
e2d6a3cb00 | ||
|
|
9e453b7890 | ||
|
|
0b8faa9e8b | ||
|
|
00bc2a15e0 | ||
|
|
432abcf85c | ||
|
|
4b6d3f6266 | ||
|
|
255ae72ed2 | ||
|
|
77dd9fb43e | ||
|
|
5dc2ae2f8d | ||
|
|
58440723bd | ||
|
|
bfdd656316 | ||
|
|
7408823c02 | ||
|
|
856ffbc4c5 | ||
|
|
d0f8463bbf | ||
|
|
162d3e22a2 | ||
|
|
4b06559ab8 | ||
|
|
9afa2e5786 | ||
|
|
7196fb2f32 | ||
|
|
8ec60faa04 | ||
|
|
13bbdd5956 | ||
|
|
3675d2f447 | ||
|
|
efc7a1d3b6 | ||
|
|
7166f9fe3d | ||
|
|
3405de38e7 | ||
|
|
2bd927f2a8 | ||
|
|
9718172fdd | ||
|
|
472a7a8309 | ||
|
|
7a7627f57e | ||
|
|
064d9aeb35 | ||
|
|
eed0baccac | ||
|
|
72480d82c8 | ||
|
|
3de710c8b5 | ||
|
|
8ebf48cb36 | ||
|
|
5083037b73 | ||
|
|
632e7e93c6 | ||
|
|
ec7139c809 | ||
|
|
752d6dc778 | ||
|
|
6d60b7b003 | ||
|
|
675e2e970a | ||
|
|
70bef528c5 | ||
|
|
78319fead5 | ||
|
|
6944f69110 | ||
|
|
1d01c75448 | ||
|
|
cb0a7bdf0c | ||
|
|
a7dba6fd1b | ||
|
|
981d923e5e | ||
|
|
6f95a2f3ad | ||
|
|
63c74461b2 | ||
|
|
e85dc36fea | ||
|
|
43d30b0fdd | ||
|
|
acba91322c | ||
|
|
ba7ce3727d | ||
|
|
d168806744 | ||
|
|
919c05eb9d | ||
|
|
2370fd72ed | ||
|
|
aef047a658 | ||
|
|
d5cdd094e8 | ||
|
|
9c71573934 | ||
|
|
8fc5ed7544 | ||
|
|
1fd932dbc6 | ||
|
|
539ac40e3d | ||
|
|
9c2656d7db | ||
|
|
161d76ee69 | ||
|
|
431c41a615 | ||
|
|
7144eb39da | ||
|
|
8e1c89ad6d | ||
|
|
7f692c0b52 | ||
|
|
86043132a8 | ||
|
|
23b4d25464 | ||
|
|
22514d8603 | ||
|
|
b440a89b13 | ||
|
|
cbdb5aff5e | ||
|
|
a3a9258a78 | ||
|
|
9f30ac8eb7 | ||
|
|
e11c9dd5c6 | ||
|
|
520b89af4a | ||
|
|
ba9aef2454 | ||
|
|
509b12cd97 | ||
|
|
40b4d51895 | ||
|
|
af68f8b1ff | ||
|
|
9cb6b29c93 | ||
|
|
3174a39ef9 | ||
|
|
78231aff31 | ||
|
|
39f1231f90 | ||
|
|
ed9251faac | ||
|
|
49d0afa616 | ||
|
|
844860ee1a |
2
.github/workflows/autoupdate.yml
vendored
2
.github/workflows/autoupdate.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
# will not occur.
|
||||
- name: "Dependabot metadata"
|
||||
id: "dependabot-metadata"
|
||||
uses: "dependabot/fetch-metadata@v1"
|
||||
uses: "dependabot/fetch-metadata@v2"
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# Here the PR gets approved.
|
||||
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -19,6 +19,8 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- uses: "actions/setup-node@v4"
|
||||
with:
|
||||
cache: "yarn"
|
||||
@@ -35,6 +37,8 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
@@ -48,6 +52,8 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
@@ -66,13 +72,15 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "poetry"
|
||||
- run: "SKIP_BUILD_FRONTEND=true poetry install"
|
||||
- run: "PYTHONPATH=yacv_server YACV_DISABLE_SERVER=true poetry run python example/object.py"
|
||||
- run: "YACV_DISABLE_SERVER=true poetry run python example/object.py"
|
||||
- uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "example"
|
||||
|
||||
21
.github/workflows/deploy1.yml
vendored
21
.github/workflows/deploy1.yml
vendored
@@ -3,12 +3,28 @@ on:
|
||||
tags:
|
||||
- "v**"
|
||||
|
||||
permissions: # Same as deploy2.yml
|
||||
contents: "write"
|
||||
pages: "write"
|
||||
id-token: "write"
|
||||
|
||||
jobs:
|
||||
|
||||
update-versions:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
with: # Ensure we are not in a detached HEAD state
|
||||
ref: "master"
|
||||
# Check that the tag commit is the latest master commit
|
||||
- run: |
|
||||
git fetch --tags
|
||||
tag_commit=$(git rev-parse ${{ github.ref }})
|
||||
master_commit=$(git rev-parse master)
|
||||
if [ "$tag_commit" != "$master_commit" ]; then
|
||||
echo "The tag commit ($tag_commit) is not the latest master commit ($master_commit)"
|
||||
exit 1
|
||||
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"
|
||||
@@ -27,7 +43,7 @@ jobs:
|
||||
if git commit -am "Automatically update version to $CLEAN_VERSION"; then
|
||||
git push
|
||||
# Move the tag to the new commit
|
||||
git tag -f -a "$VERSION" -m "$VERSION"
|
||||
git tag -f -a "v$CLEAN_VERSION" -m "v$CLEAN_VERSION"
|
||||
git push -f --tags # Force push the tag to GitHub
|
||||
# The tag move will NOT trigger a new workflow
|
||||
else
|
||||
@@ -35,8 +51,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rebuild: # Makes sure all artifacts are updated and use the new version for the next deployment steps
|
||||
deploy: # Makes sure all artifacts are updated and use the new version for the next deployment steps
|
||||
needs: "update-versions"
|
||||
uses: "./.github/workflows/deploy2.yml"
|
||||
secrets: "inherit" # Inherit the secrets from the parent workflow
|
||||
with:
|
||||
ref: "master" # Ensure we are cloning the latest version of the repository
|
||||
|
||||
39
.github/workflows/deploy2.yml
vendored
39
.github/workflows/deploy2.yml
vendored
@@ -21,7 +21,6 @@ concurrency:
|
||||
jobs:
|
||||
|
||||
rebuild: # Makes sure all artifacts are updated and use the new version
|
||||
needs: "update-versions"
|
||||
uses: "./.github/workflows/build.yml"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
@@ -33,25 +32,15 @@ jobs:
|
||||
name: "github-pages"
|
||||
url: "${{ steps.deployment.outputs.page_url }}"
|
||||
steps:
|
||||
- uses: "dawidd6/action-download-artifact@v3"
|
||||
with:
|
||||
workflow: "build.yml"
|
||||
name: "frontend"
|
||||
- uses: "actions/download-artifact@v4"
|
||||
with: # Downloads all artifacts from the build job
|
||||
path: "./public"
|
||||
allow_forks: false
|
||||
- uses: "dawidd6/action-download-artifact@v3"
|
||||
with:
|
||||
workflow: "build.yml"
|
||||
name: "logo"
|
||||
path: "./public"
|
||||
allow_forks: false
|
||||
- uses: "dawidd6/action-download-artifact@v3"
|
||||
with:
|
||||
workflow: "build.yml"
|
||||
name: "example"
|
||||
path: "./public"
|
||||
allow_forks: false
|
||||
- uses: "actions/configure-pages@v4"
|
||||
- run: | # Merge the subdirectories of public into a single directory
|
||||
for dir in public/*; do
|
||||
mv "$dir/"* public/
|
||||
rmdir "$dir"
|
||||
done
|
||||
- uses: "actions/configure-pages@v5"
|
||||
- uses: "actions/upload-pages-artifact@v3"
|
||||
with:
|
||||
path: 'public'
|
||||
@@ -72,7 +61,15 @@ jobs:
|
||||
- uses: "actions/checkout@v4"
|
||||
with:
|
||||
ref: "${{ inputs.ref }}"
|
||||
- uses: "JRubics/poetry-publish@v2"
|
||||
- uses: "actions/setup-node@v4"
|
||||
with:
|
||||
cache: "yarn"
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
pypi_token: "${{ secrets.PYPI_TOKEN }}"
|
||||
cache: "poetry"
|
||||
- run: "poetry install"
|
||||
- run: "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}"
|
||||
- run: "poetry publish --build"
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@ 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.
|
||||
- View and interact with topological entities: faces, edges, vertices and locations.
|
||||
- Control clipping planes and transparency of each model.
|
||||
- View and interact with topological entities: faces, edges, vertices and locations.
|
||||
- Select any entity and measure bounding box size and distances.
|
||||
- Fully-featured static deployment: just upload the viewer and models to your server.
|
||||
- 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.
|
||||
|
||||
## 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=base.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=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||
(or
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=base.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=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
||||
|
||||

|
||||
|
||||
@@ -211,11 +211,12 @@ Apache License
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- source-map-js@1.0.2
|
||||
- source-map-js@1.2.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
These packages each contain the following license and notice below:
|
||||
|
||||
Copyright (c) 2009-2011, Mozilla Foundation and contributors
|
||||
All rights reserved.
|
||||
@@ -1290,7 +1291,7 @@ third-party archives.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- typescript@5.4.2
|
||||
- typescript@5.4.3
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
@@ -1764,6 +1765,36 @@ SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- three-mesh-bvh@0.7.3
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Garrett Johnson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- napi-build-utils@1.0.2
|
||||
@@ -2408,7 +2439,7 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- vuetify@3.5.8
|
||||
- vuetify@3.5.13
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
@@ -2690,9 +2721,9 @@ THE SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @gltf-transform/core@3.10.0
|
||||
- @gltf-transform/extensions@3.10.0
|
||||
- @gltf-transform/functions@3.10.0
|
||||
- @gltf-transform/core@3.10.1
|
||||
- @gltf-transform/extensions@3.10.1
|
||||
- @gltf-transform/functions@3.10.1
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
|
||||
@@ -2843,7 +2874,7 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- postcss@8.4.35
|
||||
- postcss@8.4.38
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ from yacv_server import show, export_all # Check out other exported methods for
|
||||
# Create a simple object
|
||||
with BuildPart() as example:
|
||||
Box(10, 10, 5)
|
||||
Cylinder(4, 5, mode=Mode.SUBTRACT)
|
||||
Cylinder(3, 5, mode=Mode.SUBTRACT)
|
||||
|
||||
# Show it in the frontend with hot-reloading
|
||||
show(example)
|
||||
|
||||
|
||||
# %%
|
||||
|
||||
# If running on CI, export the objects to .glb files for a static deployment
|
||||
|
||||
@@ -7,7 +7,7 @@ import Tools from "./tools/Tools.vue";
|
||||
import Models from "./models/Models.vue";
|
||||
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
|
||||
import {settings} from "./misc/settings";
|
||||
import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
|
||||
import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
|
||||
import {SceneMgr} from "./misc/scene";
|
||||
import {Document} from "@gltf-transform/core";
|
||||
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
|
||||
@@ -28,34 +28,56 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
||||
const sceneDocument = shallowRef(new Document());
|
||||
provide('sceneDocument', {sceneDocument});
|
||||
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
||||
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
|
||||
const disableTap = ref(false);
|
||||
const setDisableTap = (val: boolean) => disableTap.value = val;
|
||||
provide('disableTap', {disableTap, setDisableTap});
|
||||
|
||||
async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
// Trigger progress bar as soon as possible (also triggered earlier for each raw notification)
|
||||
if (viewer.value && event.models.length > 0) {
|
||||
viewer.value.onProgress(0.10);
|
||||
}
|
||||
// Load/unload a new batch of models to optimize rendering time
|
||||
console.log("Received model update request", event.models);
|
||||
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null);
|
||||
let shutdownRequest = null;
|
||||
if (shutdownRequestIndex !== -1) {
|
||||
console.log("Will shut down the connection after this load, as requested by the server");
|
||||
shutdownRequest = event.models.splice(shutdownRequestIndex, 1)[0];
|
||||
}
|
||||
let doc = sceneDocument.value;
|
||||
for (let modelIndex in event.models) {
|
||||
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
||||
let model = event.models[modelIndex];
|
||||
if (!model.isRemove) {
|
||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast, isLast);
|
||||
} else {
|
||||
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast);
|
||||
tools.value?.removeObjectSelections(model.name);
|
||||
try {
|
||||
if (!model.isRemove) {
|
||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
|
||||
} else {
|
||||
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error loading model", model, e);
|
||||
}
|
||||
}
|
||||
if (shutdownRequest !== null) {
|
||||
console.log("Shutting down the connection as requested by the server");
|
||||
event.disconnect();
|
||||
}
|
||||
sceneDocument.value = doc
|
||||
triggerRef(sceneDocument); // Why not triggered automatically?
|
||||
}
|
||||
|
||||
async function onModelRemoveRequest(name: string) {
|
||||
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
|
||||
triggerRef(sceneDocument); // Why not triggered automatically?
|
||||
await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
|
||||
}));
|
||||
}
|
||||
|
||||
// Set up the load model event listener
|
||||
let networkMgr = new NetworkManager();
|
||||
networkMgr.addEventListener('update-early',
|
||||
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
|
||||
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
||||
// Start loading all configured models ASAP
|
||||
for (let model of settings.preload) {
|
||||
@@ -94,7 +116,7 @@ async function loadModelManual() {
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Tools</v-toolbar-title>
|
||||
</template>
|
||||
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
</sidebar>
|
||||
|
||||
</v-layout>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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 {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene';
|
||||
import type {SelectionInfo} from "../tools/selection";
|
||||
|
||||
|
||||
function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
|
||||
function getCenterAndVertexList(selInfo: SelectionInfo, 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) {
|
||||
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;
|
||||
@@ -18,14 +18,14 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
|
||||
}
|
||||
let center = new Vector3();
|
||||
let vertices = [];
|
||||
for (let i = 0; i < ind.count; i++) {
|
||||
let index = ind.array[i];
|
||||
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(obj.localToWorld(vertex));
|
||||
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
|
||||
center.add(vertex);
|
||||
vertices.push(vertex);
|
||||
}
|
||||
center = center.divideScalar(ind.count);
|
||||
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
|
||||
return {center, vertices};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
|
||||
* 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): {
|
||||
export function distances(a: SelectionInfo, b: SelectionInfo, scene: ModelScene): {
|
||||
min: Array<Vector3>,
|
||||
center: Array<Vector3>,
|
||||
max: Array<Vector3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Document, Scene, type Transform, WebIO, Buffer} from "@gltf-transform/core";
|
||||
import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
|
||||
import {unpartition} from "@gltf-transform/functions";
|
||||
|
||||
let io = new WebIO();
|
||||
@@ -12,9 +12,41 @@ export let extrasNameValueHelpers = "__helpers";
|
||||
*
|
||||
* 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> {
|
||||
export async function mergePartial(url: string, 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 fetch(url);
|
||||
let buffer = await response.arrayBuffer();
|
||||
networkFinished();
|
||||
|
||||
// Load the new document
|
||||
let newDoc = await io.read(url);
|
||||
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 (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")
|
||||
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||
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 (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));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// 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"
|
||||
@@ -26,7 +28,7 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
||||
if (rawColors) {
|
||||
colors = doc.createAccessor(name + 'Color')
|
||||
.setArray(new Float32Array(rawColors) as TypedArray)
|
||||
.setType('VEC3')
|
||||
.setType('VEC4')
|
||||
.setBuffer(buffer);
|
||||
}
|
||||
const material = doc.createMaterial(name + 'Material')
|
||||
@@ -39,6 +41,11 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
||||
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)
|
||||
@@ -48,21 +55,25 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
||||
* 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],
|
||||
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);
|
||||
...(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,8 +82,10 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||
* The grid is built as a box of triangles (representing lines) looking to the inside of the box.
|
||||
* This ensures that only the back of the grid is always visible, regardless of the camera position.
|
||||
*/
|
||||
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4 = new Matrix4(), divisions = 10) {
|
||||
export async 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));
|
||||
@@ -82,13 +95,25 @@ export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.002) {
|
||||
export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] {
|
||||
const rawPositions = [];
|
||||
const rawIndices = [];
|
||||
// Build the grid as triangles
|
||||
@@ -114,5 +139,5 @@ export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 =
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||
}
|
||||
buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
|
||||
return [rawPositions, rawIndices];
|
||||
}
|
||||
55
frontend/misc/lines.ts
Normal file
55
frontend/misc/lines.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {BufferGeometry} from 'three/src/core/BufferGeometry.js';
|
||||
import {Vector2} from 'three/src/math/Vector2.js';
|
||||
|
||||
// The following imports must be done dynamically to be able to import three.js separately (smaller bundle sizee)
|
||||
// import {LineSegments2} from "three/examples/jsm/lines/LineSegments2.js";
|
||||
// import {LineMaterial} from "three/examples/jsm/lines/LineMaterial.js";
|
||||
// import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
|
||||
const LineSegments2Import = import('three/examples/jsm/lines/LineSegments2.js');
|
||||
const LineMaterialImport = import('three/examples/jsm/lines/LineMaterial.js');
|
||||
const LineSegmentsGeometryImport = import('three/examples/jsm/lines/LineSegmentsGeometry.js');
|
||||
|
||||
export async function toLineSegments(bufferGeometry: BufferGeometry, lineWidth: number = 0.1) {
|
||||
const LineSegments2 = (await LineSegments2Import).LineSegments2;
|
||||
const LineMaterial = (await LineMaterialImport).LineMaterial;
|
||||
return new LineSegments2(await toLineSegmentsGeometry(bufferGeometry), new LineMaterial({
|
||||
color: 0xffffffff,
|
||||
vertexColors: true,
|
||||
linewidth: lineWidth, // mm
|
||||
worldUnits: true,
|
||||
resolution: new Vector2(1, 1), // Update resolution on resize!!!
|
||||
}));
|
||||
}
|
||||
|
||||
async function toLineSegmentsGeometry(bufferGeometry: BufferGeometry) {
|
||||
const LineSegmentsGeometry = (await LineSegmentsGeometryImport).LineSegmentsGeometry;
|
||||
const lg = new LineSegmentsGeometry();
|
||||
|
||||
const position = bufferGeometry.getAttribute('position');
|
||||
const indexAttribute = bufferGeometry.index!!;
|
||||
const positions = [];
|
||||
for (let index = 0; index != indexAttribute.count; ++index) {
|
||||
const i = indexAttribute.getX(index);
|
||||
const x = position.getX(i);
|
||||
const y = position.getY(i);
|
||||
const z = position.getZ(i);
|
||||
positions.push(x, y, z);
|
||||
}
|
||||
lg.setPositions(positions);
|
||||
|
||||
const colors = [];
|
||||
const color = bufferGeometry.getAttribute('color');
|
||||
if (color) {
|
||||
for (let index = 0; index != indexAttribute.count; ++index) {
|
||||
const i = indexAttribute.getX(index);
|
||||
const r = color.getX(i);
|
||||
const g = color.getY(i);
|
||||
const b = color.getZ(i);
|
||||
colors.push(r, g, b);
|
||||
}
|
||||
lg.setColors(colors);
|
||||
}
|
||||
|
||||
lg.userData = bufferGeometry.userData;
|
||||
return lg;
|
||||
}
|
||||
@@ -2,27 +2,29 @@ import {settings} from "./settings";
|
||||
|
||||
const batchTimeout = 250; // ms
|
||||
|
||||
class NetworkUpdateEventModel {
|
||||
export class NetworkUpdateEventModel {
|
||||
name: string;
|
||||
url: string;
|
||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||
hash: string | null;
|
||||
isRemove: boolean;
|
||||
isRemove: boolean | null; // This is null for a shutdown event
|
||||
|
||||
constructor(name: string, url: string, hash: string | null, isDelete: boolean) {
|
||||
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.hash = hash;
|
||||
this.isRemove = isDelete;
|
||||
this.isRemove = isRemove;
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkUpdateEvent extends Event {
|
||||
models: NetworkUpdateEventModel[];
|
||||
disconnect: () => void;
|
||||
|
||||
constructor(models: NetworkUpdateEventModel[]) {
|
||||
constructor(models: NetworkUpdateEventModel[], disconnect: () => void) {
|
||||
super("update");
|
||||
this.models = models;
|
||||
this.disconnect = disconnect;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,48 +59,81 @@ export class NetworkManager extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
private async monitorDevServer(url: URL) {
|
||||
try {
|
||||
// WARNING: This will spam the console logs with failed requests when the server is down
|
||||
let response = await fetch(url.toString());
|
||||
// console.log("Monitoring", url.toString(), response);
|
||||
if (response.status === 200) {
|
||||
let lines = readLinesStreamings(response.body!.getReader());
|
||||
for await (let line of lines) {
|
||||
if (!line || !line.startsWith("data:")) continue;
|
||||
let data = JSON.parse(line.slice(5));
|
||||
// console.debug("WebSocket message", data);
|
||||
let urlObj = new URL(url);
|
||||
urlObj.searchParams.delete("api_updates");
|
||||
urlObj.searchParams.set("api_object", data.name);
|
||||
this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove);
|
||||
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
|
||||
while (!stop()) {
|
||||
try {
|
||||
// WARNING: This will spam the console logs with failed requests when the server is down
|
||||
const controller = new AbortController();
|
||||
let response = await fetch(url.toString(), {signal: controller.signal});
|
||||
// console.log("Monitoring", url.toString(), response);
|
||||
if (response.status === 200) {
|
||||
let lines = readLinesStreamings(response.body!.getReader());
|
||||
for await (let line of lines) {
|
||||
if (stop()) break;
|
||||
if (!line || !line.startsWith("data:")) continue;
|
||||
let data: { name: string, hash: string, is_remove: boolean | null } = JSON.parse(line.slice(5));
|
||||
// console.debug("WebSocket message", data);
|
||||
let urlObj = new URL(url);
|
||||
urlObj.searchParams.delete("api_updates");
|
||||
urlObj.searchParams.set("api_object", data.name);
|
||||
this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove, async () => {
|
||||
controller.abort(); // Notify the server that we are done
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Server is down, wait a little longer before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
|
||||
}
|
||||
controller.abort();
|
||||
} catch (e) { // Ignore errors (retry very soon)
|
||||
}
|
||||
} catch (e) { // Ignore errors (retry very soon)
|
||||
await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
|
||||
}
|
||||
setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs);
|
||||
return;
|
||||
}
|
||||
|
||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean) {
|
||||
let prevHash = this.knownObjectHashes[name];
|
||||
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash);
|
||||
if (!hash || hash !== prevHash || isRemove) {
|
||||
if (!isRemove) {
|
||||
this.knownObjectHashes[name] = hash;
|
||||
} else {
|
||||
delete this.knownObjectHashes[name];
|
||||
}
|
||||
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||
this.bufferedUpdates.push(newModel);
|
||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
||||
}) {
|
||||
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||
|
||||
// Optimization: try to batch updates automatically for faster rendering
|
||||
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
||||
this.batchTimeout = setTimeout(() => {
|
||||
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates));
|
||||
this.bufferedUpdates = [];
|
||||
}, batchTimeout);
|
||||
}
|
||||
// We only care about the latest update per model name
|
||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
||||
|
||||
// Add the new model to the list of updates and dispatch the early update
|
||||
let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||
this.bufferedUpdates.push(upd);
|
||||
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
|
||||
|
||||
// Optimization: try to batch updates automatically for faster rendering
|
||||
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
||||
this.batchTimeout = setTimeout(() => {
|
||||
// Update known hashes for minimal updates
|
||||
for (let model of this.bufferedUpdates) {
|
||||
if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) {
|
||||
// Delete this useless update
|
||||
let foundFirst = false;
|
||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => {
|
||||
if (m === model) {
|
||||
if (!foundFirst) { // Remove only first full match
|
||||
foundFirst = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
} else {
|
||||
// Keep this update and update the last known hash
|
||||
if (model.isRemove == true) {
|
||||
if (model.name in this.knownObjectHashes) delete this.knownObjectHashes[model.name];
|
||||
} else if (model.isRemove == false) {
|
||||
this.knownObjectHashes[model.name] = model.hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the event to actually update the models
|
||||
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
||||
this.bufferedUpdates = [];
|
||||
}, batchTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,27 @@ 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, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
||||
let loadStart = performance.now();
|
||||
let loadNetworkEnd: number;
|
||||
|
||||
// Start merging into the current document, replacing or adding as needed
|
||||
document = await mergePartial(url, name, document);
|
||||
try {
|
||||
// Start merging into the current document, replacing or adding as needed
|
||||
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
||||
|
||||
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
|
||||
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
||||
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
||||
} finally {
|
||||
if (updateHelpers) {
|
||||
// Reload the helpers to fit the new model
|
||||
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
||||
reloadScene = false;
|
||||
}
|
||||
|
||||
if (updateHelpers) {
|
||||
// Reload the helpers to fit the new model
|
||||
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
||||
reloadScene = false;
|
||||
}
|
||||
|
||||
if (reloadScene) {
|
||||
// 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");
|
||||
if (reloadScene) {
|
||||
// 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;
|
||||
@@ -95,7 +99,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'});
|
||||
console.debug("Showing current doc", document, "as", Array.from(buffer));
|
||||
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||
sceneUrl.value = URL.createObjectURL(blob);
|
||||
|
||||
return document;
|
||||
|
||||
@@ -12,6 +12,8 @@ export const settings = {
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
"dev+http://127.0.0.1:32323/"
|
||||
],
|
||||
loadHelpers: true,
|
||||
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
|
||||
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
|
||||
monitorEveryMs: 100,
|
||||
monitorOpenTimeoutMs: 1000,
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
VTooltip,
|
||||
} from "vuetify/lib/components/index.mjs";
|
||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||
import {Document, Mesh} from "@gltf-transform/core";
|
||||
import {inject, ref, type ShallowRef, watch} from "vue";
|
||||
import {Mesh} from "@gltf-transform/core";
|
||||
import {ref, watch} from "vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {
|
||||
mdiCircleOpacity,
|
||||
@@ -22,16 +22,18 @@ import {
|
||||
mdiRectangle,
|
||||
mdiRectangleOutline,
|
||||
mdiSwapHorizontal,
|
||||
mdiVectorLine,
|
||||
mdiVectorRectangle
|
||||
} from '@mdi/js'
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import {SceneMgr} from "../misc/scene";
|
||||
import {BackSide, FrontSide} from "three/src/constants.js";
|
||||
import {Box3} from "three/src/math/Box3.js";
|
||||
import {Color} from "three/src/math/Color.js";
|
||||
import {Plane} from "three/src/math/Plane.js";
|
||||
import {Vector3} from "three/src/math/Vector3.js";
|
||||
import type {MObject3D} from "../tools/Selection.vue";
|
||||
import {toLineSegments} from "../misc/lines.js";
|
||||
import {settings} from "../misc/settings.js";
|
||||
|
||||
const props = defineProps<{
|
||||
meshes: Array<Mesh>,
|
||||
@@ -41,25 +43,24 @@ const emit = defineEmits<{ remove: [] }>()
|
||||
|
||||
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||
|
||||
// Reactive properties
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
const opacity = defineModel<number>("opacity", {default: 1});
|
||||
// Count the number of faces, edges and vertices
|
||||
let faceCount = ref(-1);
|
||||
let edgeCount = ref(-1);
|
||||
let vertexCount = ref(-1);
|
||||
|
||||
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
||||
const clipPlaneX = ref(1);
|
||||
const clipPlaneSwappedX = ref(false);
|
||||
const clipPlaneY = ref(1);
|
||||
const clipPlaneSwappedY = ref(false);
|
||||
const clipPlaneZ = ref(1);
|
||||
const clipPlaneSwappedZ = ref(false);
|
||||
const edgeWidth = ref(settings.edgeWidth);
|
||||
|
||||
// Count the number of faces, edges and vertices
|
||||
let faceCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length).reduce((a, b) => a + b, 0)
|
||||
let edgeCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]).length).reduce((a, b) => a + b, 0)
|
||||
let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0)
|
||||
|
||||
// Set initial defaults for the enabled features
|
||||
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||
if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||
if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||
// Misc properties
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
const opacity = defineModel<number>("opacity", {default: 1});
|
||||
const wireframe = ref(false);
|
||||
|
||||
// Listeners for changes in the properties (or viewer reloads)
|
||||
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
@@ -67,13 +68,10 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
||||
let childIsEdge = child.type == 'Line' || child.type == 'LineSegments'
|
||||
let childIsEdge = child.type == 'Line' || child.type == 'LineSegments' || child.type == 'LineSegments2'
|
||||
let childIsVertex = child.type == 'Points'
|
||||
if (childIsFace || childIsEdge || childIsVertex) {
|
||||
let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1);
|
||||
@@ -87,16 +85,12 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
watch(enabledFeatures, onEnabledFeaturesChange);
|
||||
watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
|
||||
|
||||
function onOpacityChange(newOpacity: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their opacity based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
// console.log('Opacity may have changed', newOpacity)
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material && child.material.opacity !== newOpacity) {
|
||||
@@ -111,7 +105,22 @@ function onOpacityChange(newOpacity: number) {
|
||||
|
||||
watch(opacity, onOpacityChange);
|
||||
|
||||
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
|
||||
function onWireframeChange(newWireframe: boolean) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material && child.material.wireframe !== newWireframe) {
|
||||
child.material.wireframe = newWireframe;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
watch(wireframe, onWireframeChange);
|
||||
|
||||
function onClipPlanesChange() {
|
||||
let scene = props.viewer?.scene;
|
||||
@@ -125,22 +134,25 @@ function onClipPlanesChange() {
|
||||
if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) {
|
||||
// Global value for all models, once set it cannot be unset (unknown for other models...)
|
||||
props.viewer.renderer.threeRenderer.localClippingEnabled = true;
|
||||
// Due to model-viewer's camera manipulation, the bounding box needs to be transformed
|
||||
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
|
||||
if (!boundingBox) return; // No models. Should not happen.
|
||||
bbox = boundingBox.translate(scene.getTarget());
|
||||
// Get the bounding box containing all features of this model
|
||||
bbox = new Box3();
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
bbox.expandByObject(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material) {
|
||||
if (bbox) {
|
||||
if (bbox?.isEmpty() == false) {
|
||||
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x);
|
||||
let offsetY = bbox.min.z + clipPlaneY.value * (bbox.max.z - bbox.min.z);
|
||||
let offsetZ = bbox.min.y + clipPlaneZ.value * (bbox.max.y - bbox.min.y);
|
||||
let offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y);
|
||||
let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z);
|
||||
let planes = [
|
||||
new Plane(new Vector3(-1, 0, 0), offsetX),
|
||||
new Plane(new Vector3(0, 0, 1), offsetY),
|
||||
new Plane(new Vector3(0, -1, 0), offsetZ),
|
||||
new Plane(new Vector3(0, -1, 0), offsetY),
|
||||
new Plane(new Vector3(0, 0, 1), -offsetZ),
|
||||
];
|
||||
if (clipPlaneSwappedX.value) planes[0].negate();
|
||||
if (clipPlaneSwappedY.value) planes[1].negate();
|
||||
@@ -169,17 +181,105 @@ watch(clipPlaneSwappedZ, onClipPlanesChange);
|
||||
// Clip planes are also affected by the camera position, so we need to listen to camera changes
|
||||
props.viewer!!.onElemReady((elem) => elem.addEventListener('camera-change', onClipPlanesChange))
|
||||
|
||||
let edgeWidthChangeCleanup = [] as Array<() => void>;
|
||||
|
||||
function onEdgeWidthChange(newEdgeWidth: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
edgeWidthChangeCleanup.forEach((f) => f());
|
||||
edgeWidthChangeCleanup = [];
|
||||
let linesToImprove: Array<MObject3D> = [];
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.type == 'Line' || child.type == 'LineSegments') {
|
||||
// child.material.linewidth = 3; // Not supported in WebGL2
|
||||
// Swap geometry with LineGeometry to support widths
|
||||
// https://threejs.org/examples/?q=line#webgl_lines_fat
|
||||
if (newEdgeWidth > 0) linesToImprove.push(child);
|
||||
}
|
||||
if (child.type == 'Points') {
|
||||
(child.material as any).size = newEdgeWidth > 0 ? newEdgeWidth * 50 : 5;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
linesToImprove.forEach(async (line: MObject3D) => {
|
||||
let line2 = await toLineSegments(line.geometry, newEdgeWidth);
|
||||
// Update resolution on resize
|
||||
let resizeListener = (elem: HTMLElement) => {
|
||||
line2.material.resolution.set(elem.clientWidth, elem.clientHeight);
|
||||
line2.material.needsUpdate = true;
|
||||
};
|
||||
props.viewer!!.onElemReady((elem) => {
|
||||
elem.addEventListener('resize', () => resizeListener(elem));
|
||||
resizeListener(elem);
|
||||
});
|
||||
// Copy the transform of the original line
|
||||
line2.position.copy(line.position);
|
||||
line2.computeLineDistances();
|
||||
line2.userData = Object.assign({}, line.userData);
|
||||
line.parent!.add(line2);
|
||||
line.children.forEach((o) => line2.add(o));
|
||||
line.visible = false;
|
||||
line.userData.niceLine = line2;
|
||||
// line.parent!.remove(line); // Keep it for better raycast and selection!
|
||||
line2.userData.noHit = true;
|
||||
edgeWidthChangeCleanup.push(() => {
|
||||
line2.parent!.remove(line2);
|
||||
line.visible = true;
|
||||
props.viewer!!.onElemReady((elem) => {
|
||||
elem.removeEventListener('resize', () => resizeListener(elem));
|
||||
});
|
||||
});
|
||||
});
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
watch(edgeWidth, onEdgeWidthChange);
|
||||
|
||||
function onModelLoad() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
|
||||
// Count the number of faces, edges and vertices
|
||||
const isFirstLoad = faceCount.value === -1;
|
||||
faceCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
||||
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
edgeCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
|
||||
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
vertexCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
|
||||
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
// First time: set the enabled features to all provided features
|
||||
if (isFirstLoad) {
|
||||
if (faceCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||
else if (!enabledFeatures.value.includes(0)) enabledFeatures.value.push(0)
|
||||
if (edgeCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||
else if (!enabledFeatures.value.includes(1)) enabledFeatures.value.push(1)
|
||||
if (vertexCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||
else if (!enabledFeatures.value.includes(2)) enabledFeatures.value.push(2)
|
||||
}
|
||||
|
||||
// Add darkened back faces for all face objects to improve cutting planes
|
||||
let childrenToAdd: Array<MObject3D> = [];
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
||||
// Compute a BVH for faster raycasting (MUCH faster selection)
|
||||
// @ts-ignore
|
||||
child.geometry?.computeBoundsTree({indirect: true}); // indirect to avoid changing index order
|
||||
// TODO: Accelerated raycast for lines and points (https://github.com/gkjohnson/three-mesh-bvh/issues/243)
|
||||
// TODO: ParallelMeshBVHWorker
|
||||
|
||||
// We could implement cutting planes using the stencil buffer:
|
||||
// https://threejs.org/examples/?q=clipping#webgl_clipping_stencil
|
||||
// But this is buggy for lots of models, so instead we just draw
|
||||
@@ -193,30 +293,28 @@ function onModelLoad() {
|
||||
backChild.material = child.material.clone();
|
||||
backChild.material.side = BackSide;
|
||||
backChild.material.color = new Color(0.25, 0.25, 0.25)
|
||||
backChild.userData.noHit = true;
|
||||
child.userData.backChild = backChild;
|
||||
childrenToAdd.push(backChild as MObject3D);
|
||||
}
|
||||
}
|
||||
// if (child.type == 'Line' || child.type == 'LineSegments') {
|
||||
// child.material.linewidth = 3; // Not supported in WebGL2
|
||||
// If wide lines are really needed, we need https://threejs.org/examples/?q=line#webgl_lines_fat
|
||||
// }
|
||||
if (child.type == 'Points') {
|
||||
(child.material as any).size = 5;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child));
|
||||
scene.queueRender()
|
||||
|
||||
// Furthermore...
|
||||
// Enabled features may have been reset after a reload
|
||||
onEnabledFeaturesChange(enabledFeatures.value)
|
||||
// Opacity may have been reset after a reload
|
||||
onOpacityChange(opacity.value)
|
||||
// Wireframe may have been reset after a reload
|
||||
onWireframeChange(wireframe.value)
|
||||
// Clip planes may have been reset after a reload
|
||||
onClipPlanesChange()
|
||||
// Edge width may have been reset after a reload
|
||||
onEdgeWidthChange(edgeWidth.value)
|
||||
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
// props.viewer.elem may not yet be available, so we need to wait for it
|
||||
@@ -253,6 +351,16 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
||||
<v-tooltip activator="parent">Change opacity</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiCircleOpacity"></svg-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-tooltip activator="parent">Wireframe</v-tooltip>
|
||||
<v-checkbox-btn trueIcon="mdi-triangle-outline" falseIcon="mdi-triangle" v-model="wireframe"></v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details min="0" max="1">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiVectorLine"></svg-icon>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-divider></v-divider>
|
||||
<v-slider v-model="clipPlaneX" hide-details min="0" max="1">
|
||||
@@ -271,7 +379,7 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-model="clipPlaneY" hide-details min="0" max="1">
|
||||
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Clip plane Y</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiCube" :rotate="-120"></svg-icon>
|
||||
@@ -280,14 +388,14 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
||||
<template v-slot:append>
|
||||
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
|
||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
||||
v-model="clipPlaneSwappedY">
|
||||
v-model="clipPlaneSwappedZ">
|
||||
<template v-slot:label>
|
||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
|
||||
<v-slider v-model="clipPlaneY" hide-details min="0" max="1">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Clip plane Z</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiCube"></svg-icon>
|
||||
@@ -296,7 +404,7 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
||||
<template v-slot:append>
|
||||
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
|
||||
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
|
||||
v-model="clipPlaneSwappedZ">
|
||||
v-model="clipPlaneSwappedY">
|
||||
<template v-slot:label>
|
||||
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
|
||||
</template>
|
||||
@@ -359,4 +467,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
|
||||
.mdi-checkbox-marked-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="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"/></svg>');
|
||||
}
|
||||
|
||||
.mdi-triangle { /* 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="M1 21h22L12 2"/></svg>');
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -36,9 +36,7 @@ function onRemove(mesh: Mesh) {
|
||||
}
|
||||
|
||||
function findModel(name: string) {
|
||||
console.log('Find model', name);
|
||||
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
||||
console.log('Expanded', expandedNames.value);
|
||||
}
|
||||
|
||||
defineExpose({findModel})
|
||||
|
||||
2
frontend/shims.d.ts
vendored
2
frontend/shims.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
// Avoids typescript error when importing some files
|
||||
declare module '@jamescoyle/vue-icon'
|
||||
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
||||
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
|
||||
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'
|
||||
@@ -8,7 +8,7 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import {Vector3} from "three/src/math/Vector3.js";
|
||||
import {Matrix4} from "three/src/math/Matrix4.js";
|
||||
|
||||
globalThis.THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
||||
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
||||
|
||||
const OrientationGizmo = OrientationGizmoRaw.default;
|
||||
|
||||
|
||||
@@ -6,44 +6,50 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
|
||||
import type {Intersection, Material, Mesh, Object3D} from "three";
|
||||
import {Box3, Matrix4, Raycaster, Vector3} from "three";
|
||||
import {Box3, Color, Raycaster, Vector3} from "three";
|
||||
import type ModelViewerWrapperT from "../viewer/ModelViewerWrapper.vue";
|
||||
import {extrasNameKey} from "../misc/gltf";
|
||||
import {SceneMgr} from "../misc/scene";
|
||||
import {Document} from "@gltf-transform/core";
|
||||
import {AxesColors} from "../misc/helpers";
|
||||
import {distances} from "../misc/distances";
|
||||
import {highlight, highlightUndo, hitToSelectionInfo, type SelectionInfo} from "./selection";
|
||||
|
||||
export type MObject3D = Mesh & {
|
||||
userData: { noHit?: boolean },
|
||||
material: Material & { color: { r: number, g: number, b: number }, __prevBaseColorFactor?: [number, number, number] }
|
||||
material: Material & {
|
||||
color: Color,
|
||||
wireframe?: boolean
|
||||
}
|
||||
};
|
||||
|
||||
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
|
||||
let emit = defineEmits<{ findModel: [string] }>();
|
||||
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
|
||||
let selectionEnabled = ref(false);
|
||||
let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
|
||||
let selected = defineModel<Array<SelectionInfo>>({default: []});
|
||||
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
||||
let showDistances = ref<Boolean>(true);
|
||||
|
||||
let mouseDownAt: [number, number] | null = null;
|
||||
let mouseDownTime = 0;
|
||||
let selectFilter = ref('Any (S)');
|
||||
const raycaster = new Raycaster();
|
||||
|
||||
|
||||
let selectionMoveListener = (event: MouseEvent) => {
|
||||
let mouseDownListener = (event: MouseEvent) => {
|
||||
mouseDownAt = [event.clientX, event.clientY];
|
||||
mouseDownTime = performance.now();
|
||||
if (!selectionEnabled.value) return;
|
||||
};
|
||||
|
||||
let selectionListener = (event: MouseEvent) => {
|
||||
let mouseUpListener = (event: MouseEvent) => {
|
||||
// If the mouse moved while clicked (dragging), avoid selection logic
|
||||
if (mouseDownAt) {
|
||||
let [x, y] = mouseDownAt;
|
||||
mouseDownAt = null;
|
||||
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) {
|
||||
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5 || performance.now() - mouseDownTime > 500) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -92,8 +98,27 @@ let selectionListener = (event: MouseEvent) => {
|
||||
// let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
|
||||
// setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
|
||||
|
||||
// Find all hit objects and select the wanted one based on the filter
|
||||
const hits = raycaster.intersectObject(scene, true);
|
||||
// Find all hit objects and raycast the wanted ones based on the filter
|
||||
let objects: Array<any> = [];
|
||||
scene.traverse((obj) => {
|
||||
const kind = obj.type
|
||||
let isFace = kind === 'Mesh' || kind === 'SkinnedMesh';
|
||||
let isEdge = kind === 'Line' || kind === 'LineSegments';
|
||||
let isVertex = kind === 'Points';
|
||||
if (obj.userData.noHit !== true &&
|
||||
((selectFilter.value === 'Any (S)' && (isFace || isEdge || isVertex)) ||
|
||||
(selectFilter.value === '(F)aces' && isFace) ||
|
||||
(selectFilter.value === '(E)dges' && isEdge) ||
|
||||
(selectFilter.value === '(V)ertices' && isVertex))) {
|
||||
objects.push(obj);
|
||||
}
|
||||
});
|
||||
//console.log("Raycasting objects", objects)
|
||||
|
||||
// Run the raycaster on the selected objects only searching for the first hit
|
||||
// @ts-ignore
|
||||
raycaster.firstHitOnly = true;
|
||||
const hits = raycaster.intersectObjects(objects, false);
|
||||
let hit = hits
|
||||
// Check feasibility
|
||||
.filter((hit: Intersection<Object3D>) => {
|
||||
@@ -106,7 +131,7 @@ let selectionListener = (event: MouseEvent) => {
|
||||
(isFace && selectFilter.value === '(F)aces') ||
|
||||
(isEdge && selectFilter.value === '(E)dges') ||
|
||||
(isVertex && selectFilter.value === '(V)ertices');
|
||||
return (!isFace || hit.object.visible) && !hit.object.userData.noHit && kindOk;
|
||||
return (!isFace || hit.object.visible) && kindOk;
|
||||
})
|
||||
// Sort for highlighting partially hidden edges/vertices
|
||||
.sort((a, b) => {
|
||||
@@ -123,17 +148,19 @@ let selectionListener = (event: MouseEvent) => {
|
||||
})
|
||||
// Return the best hit
|
||||
[0] as Intersection<MObject3D> | undefined;
|
||||
// console.log('Hit', hit)
|
||||
|
||||
if (!highlightNextSelection.value[0]) {
|
||||
// If we are selecting, toggle the selection or deselect all if no hit
|
||||
if (hit) {
|
||||
let selInfo: SelectionInfo | null = null;
|
||||
if (hit) selInfo = hitToSelectionInfo(hit);
|
||||
//console.log('Hit', hit, 'SelInfo', selInfo);
|
||||
if (hit && selInfo !== null) {
|
||||
// Toggle selection
|
||||
const wasSelected = selected.value.find((m) => m.object.name === hit?.object?.name) !== undefined;
|
||||
const wasSelected = selected.value.find((m) => m.getKey() === selInfo.getKey()) !== undefined;
|
||||
if (wasSelected) {
|
||||
deselect(hit)
|
||||
deselect(selInfo)
|
||||
} else {
|
||||
select(hit)
|
||||
select(selInfo)
|
||||
}
|
||||
} else {
|
||||
deselectAll();
|
||||
@@ -149,34 +176,22 @@ let selectionListener = (event: MouseEvent) => {
|
||||
scene.queueRender() // Force rerender of model-viewer
|
||||
}
|
||||
|
||||
function select(hit: Intersection<MObject3D>) {
|
||||
// console.log('Selecting', hit.object.name)
|
||||
if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) {
|
||||
selected.value.push(hit);
|
||||
function select(selInfo: SelectionInfo) {
|
||||
// console.log('Selecting', selInfo.object.name)
|
||||
if (selected.value.find((m) => m.getKey() === selInfo.getKey()) === undefined) {
|
||||
selected.value.push(selInfo);
|
||||
}
|
||||
hit.object.material.__prevBaseColorFactor = [
|
||||
hit.object.material.color.r,
|
||||
hit.object.material.color.g,
|
||||
hit.object.material.color.b,
|
||||
];
|
||||
hit.object.material.color.r = 1;
|
||||
hit.object.material.color.g = 0;
|
||||
hit.object.material.color.b = 0;
|
||||
highlight(selInfo);
|
||||
}
|
||||
|
||||
function deselect(hit: Intersection<MObject3D>, alsoRemove = true) {
|
||||
// console.log('Deselecting', hit.object.name)
|
||||
function deselect(selInfo: SelectionInfo, alsoRemove = true) {
|
||||
// console.log('Deselecting', selInfo.object.name)
|
||||
if (alsoRemove) {
|
||||
// Remove the matching object from the selection
|
||||
let toRemove = selected.value.findIndex((m) => m.object.name === hit.object.name);
|
||||
let toRemove = selected.value.findIndex((m) => m.getKey() === selInfo.getKey());
|
||||
selected.value.splice(toRemove, 1);
|
||||
}
|
||||
if (hit.object.material.__prevBaseColorFactor) {
|
||||
hit.object.material.color.r = hit.object.material.__prevBaseColorFactor[0]
|
||||
hit.object.material.color.g = hit.object.material.__prevBaseColorFactor[1]
|
||||
hit.object.material.color.b = hit.object.material.__prevBaseColorFactor[2]
|
||||
delete hit.object.material.__prevBaseColorFactor;
|
||||
}
|
||||
highlightUndo(selInfo);
|
||||
}
|
||||
|
||||
function deselectAll(alsoRemove = true) {
|
||||
@@ -241,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
|
||||
viewer.onElemReady((elem: ModelViewerElement) => {
|
||||
if (hasListeners) return;
|
||||
hasListeners = true;
|
||||
elem.addEventListener('mouseup', selectionListener);
|
||||
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mouseup', mouseUpListener);
|
||||
elem.addEventListener('load', () => {
|
||||
// After a reload of the scene, we need to recover object references and highlight them again
|
||||
for (let sel of selected.value) {
|
||||
let scene = props.viewer?.scene;
|
||||
if (!scene) continue;
|
||||
let foundObject = null;
|
||||
scene.traverse((obj: MObject3D) => {
|
||||
if (sel.matches(obj)) {
|
||||
foundObject = obj as MObject3D;
|
||||
}
|
||||
});
|
||||
if (foundObject) {
|
||||
sel.object = foundObject;
|
||||
highlight(sel);
|
||||
} else {
|
||||
selected.value = selected.value.filter((m) => m.getKey() !== sel.getKey());
|
||||
}
|
||||
}
|
||||
if (firstLoad) {
|
||||
toggleShowBoundingBox();
|
||||
firstLoad = false;
|
||||
} else {
|
||||
updateBoundingBox();
|
||||
}
|
||||
});
|
||||
elem.addEventListener('camera-change', onCameraChange);
|
||||
@@ -273,9 +303,8 @@ function updateBoundingBox() {
|
||||
if (selected.value.length > 0) {
|
||||
bb = new Box3();
|
||||
for (let hit of selected.value) {
|
||||
bb.expandByObject(hit.object);
|
||||
bb.union(hit.getBox())
|
||||
}
|
||||
bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget()));
|
||||
} else {
|
||||
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
|
||||
if (!boundingBox) return; // No models. Should not happen.
|
||||
@@ -380,9 +409,7 @@ function updateDistances() {
|
||||
}
|
||||
|
||||
// Add lines (if not already added)
|
||||
let objA = selected.value[0].object;
|
||||
let objB = selected.value[1].object;
|
||||
let {min, center, max} = distances(objA, objB, props.viewer?.scene);
|
||||
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");
|
||||
@@ -396,6 +423,8 @@ function updateDistances() {
|
||||
return;
|
||||
}
|
||||
|
||||
defineExpose({deselect, updateBoundingBox, updateDistances});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 's') {
|
||||
|
||||
@@ -16,11 +16,11 @@ import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
|
||||
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {Intersection} from "three";
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import Loading from "../misc/Loading.vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {defineAsyncComponent, type Ref, ref} from "vue";
|
||||
import {defineAsyncComponent, ref, type Ref} from "vue";
|
||||
import type {SelectionInfo} from "./selection";
|
||||
|
||||
const SelectionComponent = defineAsyncComponent({
|
||||
loader: () => import("./Selection.vue"),
|
||||
@@ -39,10 +39,10 @@ const LicensesDialogContent = defineAsyncComponent({
|
||||
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
|
||||
const emit = defineEmits<{ findModel: [string] }>()
|
||||
|
||||
let selection: Ref<Array<Intersection<MObject3D>>> = ref([]);
|
||||
let selectionFaceCount = () => selection.value.filter((s) => s.object.type == "Mesh" || s.object.type == "SkinnedMesh").length
|
||||
let selectionEdgeCount = () => selection.value.filter((s) => s.object.type == "Line").length
|
||||
let selectionVertexCount = () => selection.value.filter((s) => s.object.type == "Points").length
|
||||
let selection: Ref<Array<SelectionInfo>> = ref([]);
|
||||
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
|
||||
let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
|
||||
let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
|
||||
|
||||
function syncOrthoCamera(force: boolean) {
|
||||
let scene = props.viewer?.scene;
|
||||
@@ -107,6 +107,15 @@ async function openGithub() {
|
||||
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank')
|
||||
}
|
||||
|
||||
function removeObjectSelections(objName: string) {
|
||||
for (let selInfo of selection.value.filter((s) => s.getObjectName() === objName)) {
|
||||
selectionComp.value?.deselect(selInfo);
|
||||
}
|
||||
selectionComp.value?.updateBoundingBox();
|
||||
selectionComp.value?.updateDistances();
|
||||
}
|
||||
|
||||
defineExpose({removeObjectSelections});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
@@ -133,7 +142,7 @@ window.addEventListener('keydown', (event) => {
|
||||
</v-btn>
|
||||
<v-divider/>
|
||||
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
|
||||
<selection-component :ref="selectionComp as any" :viewer="props.viewer as any" v-model="selection"
|
||||
<selection-component ref="selectionComp" :viewer="props.viewer as any" v-model="selection"
|
||||
@findModel="(name) => emit('findModel', name)"/>
|
||||
<v-divider/>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -187,4 +196,8 @@ window.addEventListener('keydown', (event) => {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
163
frontend/tools/selection.ts
Normal file
163
frontend/tools/selection.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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";
|
||||
|
||||
/** 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]
|
||||
|
||||
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 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 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]);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function hitFaceTriangleIndices(hit: Intersection<MObject3D>): [number, number] | null {
|
||||
let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
|
||||
if (hit.faceIndex === undefined) 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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
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]];
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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]);
|
||||
}
|
||||
|
||||
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()];
|
||||
}
|
||||
@@ -3,14 +3,21 @@ import {settings} from "../misc/settings";
|
||||
import {inject, onMounted, type Ref, ref, watch} from "vue";
|
||||
import {VList, VListItem} from "vuetify/lib/components/index.mjs";
|
||||
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||
import Loading from "../misc/Loading.vue";
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
||||
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
||||
import type {Vector3} from "three";
|
||||
import {BufferGeometry, Mesh} from "three";
|
||||
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
|
||||
|
||||
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
||||
//@ts-ignore
|
||||
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
||||
//@ts-ignore
|
||||
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
||||
//@ts-ignore
|
||||
Mesh.prototype.raycast = acceleratedRaycast;
|
||||
|
||||
const emit = defineEmits<{ load: [] }>()
|
||||
|
||||
@@ -34,8 +41,31 @@ onMounted(() => {
|
||||
emit('load')
|
||||
});
|
||||
elem.value.addEventListener('camera-change', onCameraChange);
|
||||
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
|
||||
});
|
||||
|
||||
// Handles loading the events for <model-viewer>'s slotted progress bar
|
||||
const progressBar = ref<HTMLElement | null>(null);
|
||||
const updateBar = ref<HTMLElement | null>(null);
|
||||
let onProgressHideTimeout: number | null = null;
|
||||
const onProgress = (totalProgress: number) => {
|
||||
if (!progressBar.value || !updateBar.value) return;
|
||||
// Update the progress bar and ensure it's visible
|
||||
progressBar.value.style.display = 'block';
|
||||
progressBar.value.style.opacity = '1'; // Fade in
|
||||
updateBar.value.style.width = `${totalProgress * 100}%`;
|
||||
// Auto-hide smoothly when no progress is made for a while
|
||||
if (onProgressHideTimeout) clearTimeout(onProgressHideTimeout);
|
||||
onProgressHideTimeout = setTimeout(() => {
|
||||
if (!progressBar.value) return;
|
||||
progressBar.value.style.opacity = '0'; // Fade out
|
||||
setTimeout(() => {
|
||||
if (!progressBar.value) return;
|
||||
progressBar.value.style.display = 'none'; // Actually hide
|
||||
}, 300); // 0.3s fade out
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
class Line3DData {
|
||||
startHotspot: HTMLElement = document.body
|
||||
endHotspot: HTMLElement = document.body
|
||||
@@ -130,7 +160,7 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
|
||||
return Object.entries(lines);
|
||||
}
|
||||
|
||||
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D});
|
||||
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D, onProgress});
|
||||
|
||||
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
||||
watch(disableTap, (value) => {
|
||||
@@ -148,13 +178,19 @@ watch(disableTap, (value) => {
|
||||
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
|
||||
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
|
||||
:environment-image="settings.background">
|
||||
<slot></slot> <!-- Controls, annotations, etc. -->
|
||||
<slot></slot>
|
||||
<!-- Display some information during initial load -->
|
||||
<div class="annotation initial-load-banner">
|
||||
Trying to load models from...
|
||||
<v-list v-for="src in settings.preload" :key="src">
|
||||
<v-list-item>{{ src }}</v-list-item>
|
||||
</v-list>
|
||||
<loading></loading>
|
||||
<!-- Too much idle CPU usage: <loading></loading> -->
|
||||
</div>
|
||||
|
||||
<!-- Customize the progress bar -->
|
||||
<div class="progress-bar" slot="progress-bar" ref="progressBar">
|
||||
<div class="update-bar" ref="updateBar"/>
|
||||
</div>
|
||||
</model-viewer>
|
||||
|
||||
@@ -216,4 +252,30 @@ watch(disableTap, (value) => {
|
||||
.initial-load-banner .v-list-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
max-height: 2%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate3d(-50%, 0%, 0);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.5), 0 0 5px 1px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.update-bar {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 25px;
|
||||
float: left;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yet-another-cad-viewer",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.6",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -15,20 +15,22 @@
|
||||
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gltf-transform/core": "^3.10.0",
|
||||
"@gltf-transform/functions": "^3.10.0",
|
||||
"@gltf-transform/core": "^3.10.1",
|
||||
"@gltf-transform/extensions": "^3.10.1",
|
||||
"@gltf-transform/functions": "^3.10.1",
|
||||
"@google/model-viewer": "^3.4.0",
|
||||
"@jamescoyle/vue-icon": "^0.1.2",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/svg": "^7.4.47",
|
||||
"three": "^0.160.1",
|
||||
"three-mesh-bvh": "^0.7.3",
|
||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||
"vue": "^3.4.21",
|
||||
"vuetify": "^3.5.8"
|
||||
"vuetify": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/node": "^20.11.25",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.12.2",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
@@ -37,9 +39,9 @@
|
||||
"commander": "^12.0.0",
|
||||
"generate-license-file": "^3.0.1",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"terser": "^5.29.1",
|
||||
"typescript": "~5.4.2",
|
||||
"vite": "^5.1.5",
|
||||
"vue-tsc": "^2.0.6"
|
||||
"terser": "^5.30.0",
|
||||
"typescript": "~5.4.3",
|
||||
"vite": "^5.2.7",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
135
poetry.lock
generated
135
poetry.lock
generated
@@ -34,12 +34,12 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "build123d"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "A python CAD programming library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "build123d-0.4.0-py3-none-any.whl", hash = "sha256:1a94089dea2427e09cae9b023c01ac9cb6fd0a61faf015252eb139af4263e180"},
|
||||
{file = "build123d-0.5.0-py3-none-any.whl", hash = "sha256:d0a4e82cdb0e53ef21fca8d2c84124351d7c7070077b5efa173d789002c8194c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -50,7 +50,7 @@ ipython = ">=8.0.0,<9"
|
||||
numpy = ">=1.24.1,<2"
|
||||
numpy-stl = ">=3.0.0,<4"
|
||||
ocpsvg = "*"
|
||||
py-lib3mf = "*"
|
||||
py-lib3mf = ">=2.3.1"
|
||||
svgpathtools = ">=1.5.1,<2"
|
||||
trianglesolver = "*"
|
||||
typing-extensions = ">=4.6.0,<5"
|
||||
@@ -218,53 +218,53 @@ draw5 = ["Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib"]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.49.0"
|
||||
version = "4.50.0"
|
||||
description = "Tools to manipulate font files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"},
|
||||
{file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"},
|
||||
{file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"},
|
||||
{file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"},
|
||||
{file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"},
|
||||
{file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"},
|
||||
{file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"},
|
||||
{file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"},
|
||||
{file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"},
|
||||
{file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77844e2f1b0889120b6c222fc49b2b75c3d88b930615e98893b899b9352a27ea"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"},
|
||||
{file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"},
|
||||
{file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"},
|
||||
{file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"},
|
||||
{file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"},
|
||||
{file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -318,17 +318,6 @@ qtconsole = ["qtconsole"]
|
||||
test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"]
|
||||
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"]
|
||||
|
||||
[[package]]
|
||||
name = "iterators"
|
||||
version = "0.2.0"
|
||||
description = "Iterator utility classes and functions"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "iterators-0.2.0-py3-none-any.whl", hash = "sha256:1d7ff03f576c9de0e01bac66209556c066d6b1fc45583a99cfc9f4645be7900e"},
|
||||
{file = "iterators-0.2.0.tar.gz", hash = "sha256:e9927a1ea1ef081830fd1512f3916857c36bd4b37272819a6cd29d0f44431b97"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.1"
|
||||
@@ -350,13 +339,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.21.0"
|
||||
version = "3.21.1"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "marshmallow-3.21.0-py3-none-any.whl", hash = "sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"},
|
||||
{file = "marshmallow-3.21.0.tar.gz", hash = "sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b"},
|
||||
{file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"},
|
||||
{file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -473,13 +462,13 @@ dev = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
version = "24.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
|
||||
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -637,12 +626,12 @@ tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "py-lib3mf"
|
||||
version = "2.2.0"
|
||||
version = "2.3.1"
|
||||
description = "A python package for Lib3MF tools"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "py_lib3mf-2.2.0-py3-none-any.whl", hash = "sha256:d6d3c4e3d2f65d164b810c7c4d167cea3a75b7b68efd74e07d6f63fa6d527fa1"},
|
||||
{file = "py_lib3mf-2.3.1-py3-none-any.whl", hash = "sha256:86a870ef386debba9b74683d3a08125a34c153aaa65e967f61677cc5a0a65e24"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -676,13 +665,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.1.1"
|
||||
version = "3.1.2"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
files = [
|
||||
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
|
||||
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
|
||||
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
|
||||
{file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -819,18 +808,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.1"
|
||||
version = "5.14.2"
|
||||
description = "Traitlets Python configuration system"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"},
|
||||
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"},
|
||||
{file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"},
|
||||
{file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
|
||||
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"]
|
||||
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"]
|
||||
|
||||
[[package]]
|
||||
name = "trianglesolver"
|
||||
@@ -962,4 +951,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "d9746e99dd8861758730e68d12dc72d9ec5fb0101b3c070a7d7a373439c658a0"
|
||||
content-hash = "612c2f4fcc3ff9e37bc9e604bf092452138843ec4dc529dadc210887f0e728fd"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "yacv-server"
|
||||
version = "0.6.0"
|
||||
version = "0.8.6"
|
||||
description = "Yet Another CAD Viewer (server)"
|
||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
@@ -14,12 +14,11 @@ include = [
|
||||
python = "^3.9"
|
||||
|
||||
# CAD
|
||||
build123d = "^0.4.0"
|
||||
build123d = "^0.5.0"
|
||||
|
||||
# Misc
|
||||
pygltflib = "^1.16.2"
|
||||
pillow = "^10.2.0"
|
||||
iterators = "^0.2.0"
|
||||
|
||||
[tool.poetry.build]
|
||||
generate-setup-file = false
|
||||
|
||||
@@ -26,8 +26,10 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
assetsDir: '.',
|
||||
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
|
||||
cssCodeSplit: false, // Small enough to inline
|
||||
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
|
||||
sourcemap: true, // For debugging production
|
||||
},
|
||||
define: {
|
||||
__APP_NAME__: JSON.stringify(name),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from cad import image_to_gltf
|
||||
from yacv_server.cad import image_to_gltf
|
||||
from yacv_server.yacv import YACV
|
||||
|
||||
yacv = YACV()
|
||||
@@ -15,7 +15,7 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
|
||||
# Expose some nice aliases using the default server instance
|
||||
show = yacv.show
|
||||
show_all = yacv.show_cad_all
|
||||
prepare_image = image_to_gltf
|
||||
image_to_gltf = image_to_gltf
|
||||
export_all = yacv.export_all
|
||||
remove = yacv.remove
|
||||
clear = yacv.clear
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
Utilities to work with CAD objects
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Optional, Union, List, Tuple
|
||||
import io
|
||||
import re
|
||||
from typing import Optional, Union, Tuple
|
||||
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
from build123d import Compound, Shape
|
||||
|
||||
from yacv_server.gltf import GLTFMgr
|
||||
|
||||
@@ -40,27 +45,48 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
||||
if isinstance(obj, TopoDS_Shape):
|
||||
return obj
|
||||
|
||||
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
|
||||
if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, set) or isinstance(obj, dict):
|
||||
try:
|
||||
if isinstance(obj, dict):
|
||||
obj_iter = iter(obj.values())
|
||||
else:
|
||||
obj_iter = iter(obj)
|
||||
# print(obj, ' -> ', obj_iter)
|
||||
shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
|
||||
# Silently drop non-shapes
|
||||
shapes_raw_filtered = [shape for shape in shapes_raw if shape is not None]
|
||||
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
|
||||
# Sorting is required to improve hashcode consistency
|
||||
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
|
||||
# Build a single compound shape
|
||||
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
|
||||
return get_shape(Compound(shapes_bd), error)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if error:
|
||||
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def grab_all_cad() -> List[Tuple[str, CADCoreLike]]:
|
||||
def grab_all_cad() -> set[Tuple[str, CADCoreLike]]:
|
||||
""" Grab all shapes by inspecting the stack """
|
||||
import inspect
|
||||
stack = inspect.stack()
|
||||
shapes = []
|
||||
shapes = set()
|
||||
for frame in stack:
|
||||
for key, value in frame.frame.f_locals.items():
|
||||
shape = get_shape(value, error=False)
|
||||
if shape:
|
||||
shapes.append((key, shape))
|
||||
if shape and shape not in shapes:
|
||||
shapes.add((key, shape))
|
||||
return shapes
|
||||
|
||||
|
||||
def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None,
|
||||
name: Optional[str] = None, save_mime: str = 'image/jpeg') -> Tuple[bytes, str]:
|
||||
name: Optional[str] = None, save_mime: str = 'image/jpeg', power_of_two: bool = True) \
|
||||
-> Tuple[bytes, str]:
|
||||
"""Convert an image to a GLTF CAD object."""
|
||||
from PIL import Image
|
||||
import io
|
||||
@@ -90,33 +116,40 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
if not isinstance(center_loc, TopLoc_Location):
|
||||
raise ValueError('Center location not valid')
|
||||
plane = Plane(Location(center_loc))
|
||||
# Convert coordinates system
|
||||
plane.origin = Vector(plane.origin.X, plane.origin.Z, -plane.origin.Y)
|
||||
plane.z_dir = -plane.y_dir
|
||||
plane.y_dir = plane.z_dir
|
||||
|
||||
def vert(v: Vector) -> Tuple[float, float, float]:
|
||||
return v.X, v.Y, v.Z
|
||||
|
||||
# Load the image to a byte buffer
|
||||
img = Image.open(source)
|
||||
img_buf = io.BytesIO()
|
||||
img.save(img_buf, format=format)
|
||||
img_buf = img_buf.getvalue()
|
||||
|
||||
# Build the gltf
|
||||
mgr = GLTFMgr(image=(img_buf, save_mime))
|
||||
# Use the original dimensions for scaling the model
|
||||
if width is None and height is None:
|
||||
raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm?
|
||||
elif width is None:
|
||||
width = img.width / img.height * height
|
||||
elif height is None:
|
||||
height = height or img.height / img.width * width # Apply default aspect ratio if unspecified
|
||||
|
||||
# Resize the image to a power of two if requested (recommended for GLTF)
|
||||
if power_of_two:
|
||||
new_width = 2 ** (img.width - 1).bit_length()
|
||||
new_height = 2 ** (img.height - 1).bit_length()
|
||||
img = img.resize((new_width, new_height))
|
||||
|
||||
# Save the image to a buffer
|
||||
img.save(img_buf, format=format)
|
||||
img_buf = img_buf.getvalue()
|
||||
|
||||
# Convert coordinates system as a last step (gltf is Y-up instead of Z-up)
|
||||
def vert(v: Vector) -> Vector:
|
||||
return Vector(v.X, v.Z, -v.Y)
|
||||
|
||||
# Build the gltf
|
||||
mgr = GLTFMgr(image=(img_buf, save_mime))
|
||||
mgr.add_face([
|
||||
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin - plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
], [
|
||||
(0, 2, 1),
|
||||
(0, 3, 2),
|
||||
@@ -125,7 +158,36 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
(1, 0),
|
||||
(1, 1),
|
||||
(0, 1),
|
||||
])
|
||||
], (1, 1, 1, 1))
|
||||
|
||||
# Return the GLTF binary blob and the suggested name of the image
|
||||
return b''.join(mgr.gltf.save_to_bytes()), name
|
||||
return b''.join(mgr.build().save_to_bytes()), name
|
||||
|
||||
|
||||
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
|
||||
"""Utility to compute the STABLE hash code of a shape"""
|
||||
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
||||
# This is best-effort and not guaranteed to be unique
|
||||
hasher = hashlib.md5(usedforsecurity=False)
|
||||
for k, v in extras.items():
|
||||
hasher.update(str(k).encode())
|
||||
hasher.update(str(v).encode())
|
||||
if isinstance(obj, bytes):
|
||||
hasher.update(obj)
|
||||
elif isinstance(obj, TopLoc_Location):
|
||||
sub_data = io.BytesIO()
|
||||
obj.DumpJson(sub_data)
|
||||
hasher.update(sub_data.getvalue())
|
||||
elif isinstance(obj, TopoDS_Shape):
|
||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||
for i in range(1, map_of_shapes.Extent() + 1):
|
||||
sub_shape = map_of_shapes.FindKey(i)
|
||||
sub_data = io.BytesIO()
|
||||
TopoDS_Shape.DumpJson(sub_shape, sub_data)
|
||||
val = sub_data.getvalue()
|
||||
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
||||
hasher.update(val)
|
||||
else:
|
||||
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -12,43 +12,95 @@ _checkerboard_image_bytes = base64.decodebytes(
|
||||
class GLTFMgr:
|
||||
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
||||
|
||||
def __init__(self, image: Tuple[bytes, str] = (_checkerboard_image_bytes, 'image/png')):
|
||||
gltf: GLTF2
|
||||
|
||||
# Intermediate data to be filled by the add_* methods and merged into the GLTF object
|
||||
# - Face data
|
||||
face_indices: List[int] # 3 indices per triangle
|
||||
face_positions: List[float] # x, y, z
|
||||
face_tex_coords: List[float] # u, v
|
||||
face_colors: List[float] # r, g, b, a
|
||||
image: Optional[Tuple[bytes, str]] # image/png
|
||||
# - Edge data
|
||||
edge_indices: List[int] # 2 indices per edge
|
||||
edge_positions: List[float] # x, y, z
|
||||
edge_colors: List[float] # r, g, b, a
|
||||
# - Vertex data
|
||||
vertex_indices: List[int] # 1 index per vertex
|
||||
vertex_positions: List[float] # x, y, z
|
||||
vertex_colors: List[float] # r, g, b, a
|
||||
|
||||
def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')):
|
||||
self.gltf = GLTF2(
|
||||
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
||||
scene=0,
|
||||
scenes=[Scene(nodes=[0])],
|
||||
nodes=[Node(mesh=0)],
|
||||
meshes=[Mesh(primitives=[])],
|
||||
accessors=[],
|
||||
bufferViews=[BufferView(buffer=0, byteLength=len(image[0]), byteOffset=0)],
|
||||
buffers=[Buffer(byteLength=len(image[0]))],
|
||||
samplers=[Sampler(magFilter=NEAREST)],
|
||||
textures=[Texture(source=0, sampler=0)],
|
||||
images=[Image(bufferView=0, mimeType=image[1])],
|
||||
nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
|
||||
meshes=[Mesh(primitives=[
|
||||
Primitive(indices=-1, attributes=Attributes(), mode=TRIANGLES, material=0,
|
||||
extras={"face_triangles_end": []}),
|
||||
Primitive(indices=-1, attributes=Attributes(), mode=LINES, material=0,
|
||||
extras={"edge_points_end": []}),
|
||||
Primitive(indices=-1, attributes=Attributes(), mode=POINTS, material=0),
|
||||
])],
|
||||
materials=[Material(pbrMetallicRoughness=PbrMetallicRoughness(metallicFactor=0.1, roughnessFactor=1.0),
|
||||
alphaCutoff=None)],
|
||||
)
|
||||
self.gltf.set_binary_blob(image[0])
|
||||
self.face_indices = []
|
||||
self.face_positions = []
|
||||
self.face_tex_coords = []
|
||||
self.face_colors = []
|
||||
self.image = image
|
||||
self.edge_indices = []
|
||||
self.edge_positions = []
|
||||
self.edge_colors = []
|
||||
self.vertex_indices = []
|
||||
self.vertex_positions = []
|
||||
self.vertex_colors = []
|
||||
|
||||
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
||||
tex_coord_raw: List[Tuple[float, float]]):
|
||||
"""Add a face to the GLTF as a new primitive of the unique mesh"""
|
||||
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
||||
indices = np.array([[i[0], i[1], i[2]] for i in indices_raw], dtype=np.uint32)
|
||||
tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32)
|
||||
self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material="face")
|
||||
@property
|
||||
def _faces_primitive(self) -> Primitive:
|
||||
return [p for p in self.gltf.meshes[0].primitives if p.mode == TRIANGLES][0]
|
||||
|
||||
def add_edge(self, vertices_raw: List[Tuple[float, float, float]], mat: str = None):
|
||||
"""Add an edge to the GLTF as a new primitive of the unique mesh"""
|
||||
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
||||
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32)
|
||||
tex_coord = np.array([])
|
||||
self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material=mat or "edge")
|
||||
@property
|
||||
def _edges_primitive(self) -> Primitive:
|
||||
return [p for p in self.gltf.meshes[0].primitives if p.mode == LINES][0]
|
||||
|
||||
def add_vertex(self, vertex: Tuple[float, float, float]):
|
||||
"""Add a vertex to the GLTF as a new primitive of the unique mesh"""
|
||||
vertices = np.array([[vertex[0], vertex[1], vertex[2]]])
|
||||
indices = np.array([[0]], dtype=np.uint32)
|
||||
tex_coord = np.array([], dtype=np.float32)
|
||||
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex")
|
||||
@property
|
||||
def _vertices_primitive(self) -> Primitive:
|
||||
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
|
||||
|
||||
def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
|
||||
tex_coord_raw: List[Tuple[float, float]],
|
||||
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
|
||||
"""Add a face to the GLTF mesh"""
|
||||
# assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths"
|
||||
# assert min([i for t in indices_raw for i in t]) == 0, f"Face indices start at {min(indices_raw)}"
|
||||
# assert max([e for t in indices_raw for e in t]) < len(vertices_raw), f"Indices have non-existing vertices"
|
||||
base_index = len(self.face_positions) // 3 # All the new indices reference the new vertices
|
||||
self.face_indices.extend([base_index + i for t in indices_raw for i in t])
|
||||
self.face_positions.extend([v for t in vertices_raw for v in t])
|
||||
self.face_tex_coords.extend([c for t in tex_coord_raw for c in t])
|
||||
self.face_colors.extend([col for _ in range(len(vertices_raw)) for col in color])
|
||||
self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices))
|
||||
|
||||
def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
|
||||
color: Tuple[float, float, float, float] = (0.1, 0.1, 1.0, 1.0)):
|
||||
"""Add an edge to the GLTF mesh"""
|
||||
vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc.
|
||||
base_index = len(self.edge_positions) // 3
|
||||
self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))])
|
||||
self.edge_positions.extend([v for t in vertices_flat for v in t])
|
||||
self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color])
|
||||
self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices))
|
||||
|
||||
def add_vertex(self, vertex: Tuple[float, float, float],
|
||||
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.1, 1.0)):
|
||||
"""Add a vertex to the GLTF mesh"""
|
||||
base_index = len(self.vertex_positions) // 3
|
||||
self.vertex_indices.append(base_index)
|
||||
self.vertex_positions.extend(vertex)
|
||||
self.vertex_colors.extend(color)
|
||||
|
||||
def add_location(self, loc: Location):
|
||||
"""Add a location to the GLTF as a new primitive of the unique mesh"""
|
||||
@@ -59,120 +111,91 @@ class GLTFMgr:
|
||||
|
||||
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
|
||||
self.add_vertex(vert(pl.origin))
|
||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.x_dir)], mat="locX")
|
||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.y_dir)], mat="locY")
|
||||
self.add_edge([vert(pl.origin), vert(pl.origin + pl.z_dir)], mat="locZ")
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1))
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1))
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1))
|
||||
|
||||
def add_material(self, kind: str) -> int:
|
||||
"""It is important to use a different material for each primitive to be able to change them at runtime"""
|
||||
new_material: Material
|
||||
if kind == "face":
|
||||
new_material = Material(name="face", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 1, 0.5, 1]), doubleSided=True)
|
||||
elif kind == "edge":
|
||||
new_material = Material(name="edge", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorFactor=[0, 0, 0.5, 1]))
|
||||
elif kind == "vertex":
|
||||
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorFactor=[0, 0.3, 0.3, 1]))
|
||||
elif kind == "locX":
|
||||
new_material = Material(name="locX", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorFactor=[0.97, 0.24, 0.24, 1]))
|
||||
elif kind == "locY":
|
||||
new_material = Material(name="locY", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorFactor=[0.42, 0.8, 0.15, 1]))
|
||||
elif kind == "locZ":
|
||||
new_material = Material(name="locZ", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||
baseColorFactor=[0.09, 0.55, 0.94, 1]))
|
||||
def build(self) -> GLTF2:
|
||||
"""Merge the intermediate data into the GLTF object and return it"""
|
||||
buffers_list: List[Tuple[Accessor, BufferView, bytes]] = []
|
||||
|
||||
if len(self.face_indices) > 0:
|
||||
self._faces_primitive.indices = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
|
||||
self._faces_primitive.attributes.POSITION = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(self.face_positions, 3))
|
||||
self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
|
||||
self._faces_primitive.attributes.COLOR_0 = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(self.face_colors, 4))
|
||||
else:
|
||||
raise ValueError(f"Unknown material kind {kind}")
|
||||
self.gltf.materials.append(new_material)
|
||||
return len(self.gltf.materials) - 1
|
||||
self.image = None # Unused image
|
||||
self.gltf.meshes[0].primitives = list( # Remove unused faces primitive
|
||||
filter(lambda p: p.mode != TRIANGLES, self.gltf.meshes[0].primitives))
|
||||
|
||||
def _add_any(self, vertices: np.ndarray, indices: np.ndarray, tex_coord: np.ndarray, mode: int = TRIANGLES,
|
||||
material: str = "face"):
|
||||
assert vertices.ndim == 2
|
||||
assert vertices.shape[1] == 3
|
||||
vertices = vertices.astype(np.float32)
|
||||
vertices_blob = vertices.tobytes()
|
||||
edges_and_vertices_mat = 0
|
||||
if self.image is not None and (len(self.edge_indices) > 0 or len(self.vertex_indices) > 0):
|
||||
# Create a material without texture for edges and vertices
|
||||
edges_and_vertices_mat = len(self.gltf.materials)
|
||||
new_mat = copy.deepcopy(self.gltf.materials[0])
|
||||
new_mat.pbrMetallicRoughness.baseColorTexture = None
|
||||
self.gltf.materials.append(new_mat)
|
||||
|
||||
assert indices.ndim == 2
|
||||
assert indices.shape[1] == 3 and mode == TRIANGLES or indices.shape[1] == 2 and mode == LINE_STRIP or \
|
||||
indices.shape[1] == 1 and mode == POINTS
|
||||
indices = indices.astype(np.uint32)
|
||||
indices_blob = indices.flatten().tobytes()
|
||||
# Treat edges and vertices the same way
|
||||
for (indices, positions, colors, primitive, kind) in [
|
||||
(self.edge_indices, self.edge_positions, self.edge_colors, self._edges_primitive, LINES),
|
||||
(self.vertex_indices, self.vertex_positions, self.vertex_colors, self._vertices_primitive, POINTS)
|
||||
]:
|
||||
if len(indices) > 0:
|
||||
primitive.material = edges_and_vertices_mat
|
||||
primitive.indices = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(indices, 1))
|
||||
primitive.attributes.POSITION = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(positions, 3))
|
||||
primitive.attributes.COLOR_0 = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(colors, 4))
|
||||
else:
|
||||
self.gltf.meshes[0].primitives = list( # Remove unused edges primitive
|
||||
filter(lambda p: p.mode != kind, self.gltf.meshes[0].primitives))
|
||||
|
||||
# Check that all vertices are referenced by the indices
|
||||
# This can happen on broken faces like on some fonts
|
||||
# assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}"
|
||||
# assert indices.min() == 0, f"min({indices}) != 0"
|
||||
# assert np.unique(indices.flatten()).size == len(vertices)
|
||||
if self.image is not None: # Add texture last as it creates a fake accessor that is not added!
|
||||
self.gltf.images = [Image(bufferView=len(buffers_list), mimeType=self.image[1])]
|
||||
self.gltf.textures = [Texture(source=0, sampler=0)]
|
||||
self.gltf.samplers = [Sampler(magFilter=NEAREST)]
|
||||
self.gltf.materials[0].pbrMetallicRoughness.baseColorTexture = TextureInfo(index=0)
|
||||
buffers_list.append((Accessor(), BufferView(), self.image[0]))
|
||||
|
||||
assert len(tex_coord) == 0 or tex_coord.ndim == 2
|
||||
assert len(tex_coord) == 0 or tex_coord.shape[1] == 2
|
||||
tex_coord = tex_coord.astype(np.float32)
|
||||
tex_coord_blob = tex_coord.tobytes()
|
||||
|
||||
accessor_base = len(self.gltf.accessors)
|
||||
self.gltf.meshes[0].primitives.append(
|
||||
Primitive(
|
||||
attributes=Attributes(POSITION=accessor_base + 1, TEXCOORD_0=accessor_base + 2)
|
||||
if len(tex_coord) > 0 else Attributes(POSITION=accessor_base + 1),
|
||||
indices=accessor_base,
|
||||
mode=mode,
|
||||
material=self.add_material(material),
|
||||
)
|
||||
)
|
||||
|
||||
buffer_view_base = len(self.gltf.bufferViews)
|
||||
self.gltf.accessors.extend([it for it in [
|
||||
Accessor(
|
||||
bufferView=buffer_view_base,
|
||||
componentType=UNSIGNED_INT,
|
||||
count=indices.size,
|
||||
type=SCALAR,
|
||||
max=[int(indices.max())],
|
||||
min=[int(indices.min())],
|
||||
),
|
||||
Accessor(
|
||||
bufferView=buffer_view_base + 1,
|
||||
componentType=FLOAT,
|
||||
count=len(vertices),
|
||||
type=VEC3,
|
||||
max=vertices.max(axis=0).tolist(),
|
||||
min=vertices.min(axis=0).tolist(),
|
||||
),
|
||||
Accessor(
|
||||
bufferView=buffer_view_base + 2,
|
||||
componentType=FLOAT,
|
||||
count=len(tex_coord),
|
||||
type=VEC2,
|
||||
max=tex_coord.max(axis=0).tolist(),
|
||||
min=tex_coord.min(axis=0).tolist(),
|
||||
) if len(tex_coord) > 0 else None
|
||||
] if it is not None])
|
||||
|
||||
prev_binary_blob = self.gltf.binary_blob()
|
||||
# Once all the data is ready, we can concatenate the buffers updating the accessors and views
|
||||
prev_binary_blob = self.gltf.binary_blob() or b''
|
||||
byte_offset_base = len(prev_binary_blob)
|
||||
self.gltf.bufferViews.extend([bv for bv in [
|
||||
BufferView(
|
||||
buffer=0,
|
||||
byteOffset=byte_offset_base,
|
||||
byteLength=len(indices_blob),
|
||||
target=ELEMENT_ARRAY_BUFFER,
|
||||
),
|
||||
BufferView(
|
||||
buffer=0,
|
||||
byteOffset=byte_offset_base + len(indices_blob),
|
||||
byteLength=len(vertices_blob),
|
||||
target=ARRAY_BUFFER,
|
||||
),
|
||||
BufferView(
|
||||
buffer=0,
|
||||
byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob),
|
||||
byteLength=len(tex_coord_blob),
|
||||
target=ARRAY_BUFFER,
|
||||
)
|
||||
] if bv.byteLength > 0])
|
||||
for accessor, bufferView, blob in buffers_list:
|
||||
|
||||
self.gltf.set_binary_blob(prev_binary_blob + indices_blob + vertices_blob + tex_coord_blob)
|
||||
if accessor.componentType is not None: # Remove accessor of texture
|
||||
buffer_view_base = len(self.gltf.bufferViews)
|
||||
accessor.bufferView = buffer_view_base
|
||||
self.gltf.accessors.append(accessor)
|
||||
|
||||
bufferView.buffer = 0
|
||||
bufferView.byteOffset = byte_offset_base
|
||||
bufferView.byteLength = len(blob)
|
||||
self.gltf.bufferViews.append(bufferView)
|
||||
|
||||
byte_offset_base += len(blob)
|
||||
prev_binary_blob += blob
|
||||
|
||||
self.gltf.buffers.append(Buffer(byteLength=byte_offset_base))
|
||||
self.gltf.set_binary_blob(prev_binary_blob)
|
||||
|
||||
return self.gltf
|
||||
|
||||
|
||||
def _gen_buffer_metadata(data: List[any], chunk: int) -> Tuple[Accessor, BufferView, bytes]:
|
||||
return Accessor(
|
||||
componentType={1: UNSIGNED_INT, 2: FLOAT, 3: FLOAT, 4: FLOAT}[chunk],
|
||||
count=len(data) // chunk,
|
||||
type={1: SCALAR, 2: VEC2, 3: VEC3, 4: VEC4}[chunk],
|
||||
max=[max(data[i::chunk]) for i in range(chunk)],
|
||||
min=[min(data[i::chunk]) for i in range(chunk)],
|
||||
), BufferView(
|
||||
target={1: ELEMENT_ARRAY_BUFFER, 2: ARRAY_BUFFER, 3: ARRAY_BUFFER, 4: ARRAY_BUFFER}[chunk],
|
||||
), np.array(data, dtype={1: np.uint32, 2: np.float32, 3: np.float32, 4: np.float32}[chunk]).tobytes()
|
||||
|
||||
@@ -19,16 +19,15 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
|
||||
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
|
||||
extrude(amount=1)
|
||||
|
||||
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location # Avoid overlapping:
|
||||
logo_img_location.position = Vector(logo_img_location.position.X - 4e-2, logo_img_location.position.Y,
|
||||
logo_img_location.position.Z)
|
||||
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location
|
||||
logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
|
||||
|
||||
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
|
||||
img_bytes, img_name = prepare_image(logo_img_path, logo_img_location, height=18)
|
||||
img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
|
||||
|
||||
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read()
|
||||
|
||||
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_bytes}
|
||||
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_glb_bytes}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -43,7 +42,7 @@ if __name__ == "__main__":
|
||||
# If this is not set, the server will auto-start on import and show_* calls will provide live updates
|
||||
os.environ['YACV_DISABLE_SERVER'] = 'True'
|
||||
|
||||
from yacv_server import export_all, remove, prepare_image, show
|
||||
from yacv_server import export_all, show, image_to_gltf
|
||||
|
||||
# Build the CAD part of the logo
|
||||
logo = build_logo()
|
||||
@@ -52,7 +51,8 @@ if __name__ == "__main__":
|
||||
show(*[obj for obj in logo.values()], names=[name for name in logo.keys()])
|
||||
|
||||
if testing_server:
|
||||
remove('location') # Test removing a part
|
||||
# remove('location') # Test removing a part
|
||||
pass
|
||||
else:
|
||||
# Save the complete logo to multiple GLB files
|
||||
export_all(os.path.join(ASSETS_DIR, 'logo_build'))
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import io
|
||||
import os
|
||||
import threading
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
from iterators import TimeoutIterator
|
||||
|
||||
from mylogger import logger
|
||||
from yacv_server.mylogger import logger
|
||||
|
||||
# Find the frontend folder (optional, but recommended)
|
||||
FILE_DIR = os.path.dirname(__file__)
|
||||
@@ -26,13 +23,9 @@ OBJECTS_API_PATH = '/api/object' # /{name}
|
||||
|
||||
class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
yacv: 'yacv.YACV'
|
||||
frontend_lock: threading.Lock # To avoid exiting too early while frontend makes requests
|
||||
at_least_one_client: threading.Event
|
||||
|
||||
def __init__(self, *args, yacv: 'yacv.YACV', **kwargs):
|
||||
self.yacv = yacv
|
||||
self.frontend_lock = threading.Lock()
|
||||
self.at_least_one_client = threading.Event()
|
||||
super().__init__(*args, **kwargs, directory=FRONTEND_BASE_PATH)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
@@ -77,69 +70,65 @@ class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def _api_updates(self):
|
||||
"""Handles a publish-only websocket connection that send show_object events along with their hashes and URLs"""
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
# Chunked transfer encoding!
|
||||
self.send_header("Transfer-Encoding", "chunked")
|
||||
self.end_headers()
|
||||
self.at_least_one_client.set()
|
||||
logger.debug('Updates client connected')
|
||||
|
||||
def write_chunk(_chunk_data: str):
|
||||
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
|
||||
self.wfile.write(b'\r\n')
|
||||
self.wfile.write(_chunk_data.encode('utf-8'))
|
||||
self.wfile.write(b'\r\n')
|
||||
self.wfile.flush()
|
||||
# Keep a shared read lock to know if any frontend is still working before shutting down
|
||||
with self.yacv.frontend_lock.r_locked():
|
||||
|
||||
write_chunk('retry: 100\n\n')
|
||||
# Avoid accepting new connections while shutting down
|
||||
if self.yacv.shutting_down.is_set() and self.yacv.at_least_one_client.is_set():
|
||||
self.send_error(HTTPStatus.SERVICE_UNAVAILABLE, 'Server is shutting down')
|
||||
return
|
||||
self.yacv.at_least_one_client.set()
|
||||
logger.debug('Updates client connected')
|
||||
|
||||
# Send buffered events first, while keeping a lock
|
||||
with self.frontend_lock:
|
||||
for data in self.yacv.show_events.buffer():
|
||||
logger.debug('Sending info about %s: %s', data.name, data)
|
||||
# noinspection PyUnresolvedReferences
|
||||
to_send = data.to_json()
|
||||
write_chunk(f'data: {to_send}\n\n')
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
# Chunked transfer encoding!
|
||||
self.send_header("Transfer-Encoding", "chunked")
|
||||
self.end_headers()
|
||||
|
||||
# Send future events over the same connection
|
||||
# Also send keep-alive to know if the client is still connected
|
||||
subscription = self.yacv.show_events.subscribe(include_buffered=False)
|
||||
it = TimeoutIterator(subscription, sentinel=None, reset_on_next=True, timeout=5.0) # Keep-alive interval
|
||||
try:
|
||||
for data in it:
|
||||
if data is None:
|
||||
write_chunk(':keep-alive\n\n')
|
||||
else:
|
||||
logger.debug('Sending info about %s: %s', data.name, data)
|
||||
# noinspection PyUnresolvedReferences
|
||||
to_send = data.to_json()
|
||||
write_chunk(f'data: {to_send}\n\n')
|
||||
except BrokenPipeError: # Client disconnected normally
|
||||
pass
|
||||
finally:
|
||||
logger.debug('Updates client disconnected')
|
||||
def write_chunk(_chunk_data: str):
|
||||
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
|
||||
self.wfile.write(b'\r\n')
|
||||
self.wfile.write(_chunk_data.encode('utf-8'))
|
||||
self.wfile.write(b'\r\n')
|
||||
self.wfile.flush()
|
||||
|
||||
write_chunk('retry: 100\n\n')
|
||||
|
||||
subscription = self.yacv.show_events.subscribe(yield_timeout=1.0) # Keep-alive interval
|
||||
try:
|
||||
it.interrupt()
|
||||
next(it) # Make sure the iterator is interrupted before trying to close the subscription
|
||||
for data in subscription:
|
||||
if data is None:
|
||||
write_chunk(':keep-alive\n\n')
|
||||
else:
|
||||
logger.debug('Sending info about %s: %s', data.name, data)
|
||||
# noinspection PyUnresolvedReferences
|
||||
to_send = data.to_json()
|
||||
write_chunk(f'data: {to_send}\n\n')
|
||||
except BrokenPipeError: # Client disconnected normally
|
||||
pass
|
||||
finally:
|
||||
subscription.close()
|
||||
except BaseException as e:
|
||||
logger.debug('Ignoring error while closing subscription: %s', e)
|
||||
|
||||
logger.debug('Updates client disconnected')
|
||||
|
||||
def _api_object(self, obj_name: str):
|
||||
"""Returns the object file with the matching name, building it if necessary."""
|
||||
with self.frontend_lock:
|
||||
# Export the object (or fail if not found)
|
||||
exported_glb = self.yacv.export(obj_name)
|
||||
if exported_glb is None:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found')
|
||||
return io.BytesIO()
|
||||
# Export the object (or fail if not found)
|
||||
_export = self.yacv.export(obj_name)
|
||||
if _export is None:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found')
|
||||
return io.BytesIO()
|
||||
|
||||
# Wrap the GLB in a response and return it
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header('Content-Type', 'model/gltf-binary')
|
||||
self.send_header('Content-Length', str(len(exported_glb)))
|
||||
self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"')
|
||||
self.end_headers()
|
||||
self.wfile.write(exported_glb)
|
||||
exported_glb, _hash = _export
|
||||
|
||||
# Wrap the GLB in a response and return it
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header('Content-Type', 'model/gltf-binary')
|
||||
self.send_header('Content-Length', str(len(exported_glb)))
|
||||
self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"')
|
||||
self.send_header('E-Tag', f'"{_hash}"')
|
||||
self.end_headers()
|
||||
self.wfile.write(exported_glb)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import threading
|
||||
import queue
|
||||
import queue
|
||||
import threading
|
||||
from typing import List, TypeVar, \
|
||||
@@ -8,6 +8,8 @@ from yacv_server.mylogger import logger
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
_end_of_queue = object()
|
||||
|
||||
|
||||
class BufferedPubSub(Generic[T]):
|
||||
"""A simple implementation of publish-subscribe pattern using threading and buffering all previous events"""
|
||||
@@ -45,7 +47,7 @@ class BufferedPubSub(Generic[T]):
|
||||
for event in self._buffer:
|
||||
q.put(event)
|
||||
if not include_future:
|
||||
q.put(None)
|
||||
q.put(_end_of_queue)
|
||||
return q
|
||||
|
||||
def _unsubscribe(self, q: queue.Queue[T]):
|
||||
@@ -54,14 +56,18 @@ class BufferedPubSub(Generic[T]):
|
||||
self._subscribers.remove(q)
|
||||
logger.debug(f"Unsubscribed from %s (%d subscribers)", self, len(self._subscribers))
|
||||
|
||||
def subscribe(self, include_buffered: bool = True, include_future: bool = True) -> Generator[T, None, None]:
|
||||
def subscribe(self, include_buffered: bool = True, include_future: bool = True, yield_timeout: float = 0.0) -> \
|
||||
Generator[T, None, None]:
|
||||
"""Subscribes to events as an generator that yields events and automatically unsubscribes"""
|
||||
q = self._subscribe(include_buffered, include_future)
|
||||
try:
|
||||
while True:
|
||||
v = q.get()
|
||||
try:
|
||||
v = q.get(timeout=yield_timeout)
|
||||
except queue.Empty:
|
||||
v = None
|
||||
# include_future is incompatible with None values as they are used to signal the end of the stream
|
||||
if v is None and not include_future:
|
||||
if v is _end_of_queue:
|
||||
break
|
||||
yield v
|
||||
finally: # When aclose() is called
|
||||
@@ -80,4 +86,4 @@ class BufferedPubSub(Generic[T]):
|
||||
def clear(self):
|
||||
"""Clears the buffer"""
|
||||
with self._buffer_lock:
|
||||
self._buffer.clear()
|
||||
self._buffer.clear()
|
||||
|
||||
96
yacv_server/rwlock.py
Normal file
96
yacv_server/rwlock.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" rwlock.py
|
||||
|
||||
A class to implement read-write locks on top of the standard threading
|
||||
library.
|
||||
|
||||
This is implemented with two mutexes (threading.Lock instances) as per this
|
||||
wikipedia pseudocode:
|
||||
|
||||
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes
|
||||
|
||||
Code written by Tyler Neylon at Unbox Research.
|
||||
|
||||
This file is public domain.
|
||||
"""
|
||||
|
||||
# _______________________________________________________________________
|
||||
# Imports
|
||||
|
||||
from contextlib import contextmanager
|
||||
from threading import Lock
|
||||
|
||||
|
||||
# _______________________________________________________________________
|
||||
# Class
|
||||
|
||||
class RWLock(object):
|
||||
""" RWLock class; this is meant to allow an object to be read from by
|
||||
multiple threads, but only written to by a single thread at a time. See:
|
||||
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
|
||||
|
||||
Usage:
|
||||
|
||||
from rwlock import RWLock
|
||||
|
||||
my_obj_rwlock = RWLock()
|
||||
|
||||
# When reading from my_obj:
|
||||
with my_obj_rwlock.r_locked():
|
||||
do_read_only_things_with(my_obj)
|
||||
|
||||
# When writing to my_obj:
|
||||
with my_obj_rwlock.w_locked():
|
||||
mutate(my_obj)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.w_lock = Lock()
|
||||
self.num_r_lock = Lock()
|
||||
self.num_r = 0
|
||||
|
||||
# ___________________________________________________________________
|
||||
# Reading methods.
|
||||
|
||||
def r_acquire(self, *args, **kwargs):
|
||||
self.num_r_lock.acquire(*args, **kwargs)
|
||||
self.num_r += 1
|
||||
if self.num_r == 1:
|
||||
self.w_lock.acquire(*args, **kwargs)
|
||||
self.num_r_lock.release()
|
||||
|
||||
def r_release(self, *args, **kwargs):
|
||||
assert self.num_r > 0
|
||||
self.num_r_lock.acquire(*args, **kwargs)
|
||||
self.num_r -= 1
|
||||
if self.num_r == 0:
|
||||
self.w_lock.release()
|
||||
self.num_r_lock.release()
|
||||
|
||||
@contextmanager
|
||||
def r_locked(self, *args, **kwargs):
|
||||
""" This method is designed to be used via the `with` statement. """
|
||||
try:
|
||||
self.r_acquire(*args, **kwargs)
|
||||
yield
|
||||
finally:
|
||||
self.r_release()
|
||||
|
||||
# ___________________________________________________________________
|
||||
# Writing methods.
|
||||
|
||||
def w_acquire(self, *args, **kwargs):
|
||||
self.w_lock.acquire(*args, **kwargs)
|
||||
|
||||
def w_release(self):
|
||||
self.w_lock.release()
|
||||
|
||||
@contextmanager
|
||||
def w_locked(self, *args, **kwargs):
|
||||
""" This method is designed to be used via the `with` statement. """
|
||||
try:
|
||||
self.w_acquire(*args, **kwargs)
|
||||
yield
|
||||
finally:
|
||||
self.w_release()
|
||||
@@ -1,14 +1,9 @@
|
||||
import hashlib
|
||||
import io
|
||||
import re
|
||||
from typing import List, Dict, Tuple, Union
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCP.GCPnts import GCPnts_TangentialDeflection
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
||||
from build123d import Shape, Vertex, Face, Location
|
||||
from pygltflib import GLTF2
|
||||
@@ -36,8 +31,8 @@ def tessellate(
|
||||
shape = Shape(cad_like)
|
||||
|
||||
# Perform tessellation tasks
|
||||
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
|
||||
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {}
|
||||
edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
|
||||
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
|
||||
if faces:
|
||||
for face in shape.faces():
|
||||
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
|
||||
@@ -55,7 +50,7 @@ def tessellate(
|
||||
for vertex in shape.vertices():
|
||||
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
|
||||
|
||||
return mgr.gltf
|
||||
return mgr.build()
|
||||
|
||||
|
||||
def _tessellate_face(
|
||||
@@ -65,12 +60,12 @@ def _tessellate_face(
|
||||
angular_tolerance: float = 0.1
|
||||
):
|
||||
face = Shape(ocp_face)
|
||||
face.mesh(tolerance, angular_tolerance)
|
||||
# face.mesh(tolerance, angular_tolerance)
|
||||
tri_mesh = face.tessellate(tolerance, angular_tolerance)
|
||||
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
|
||||
if poly is None:
|
||||
logger.warn("No triangulation found for face")
|
||||
return GLTF2()
|
||||
tri_mesh = face.tessellate(tolerance, angular_tolerance)
|
||||
|
||||
# Get UV of each face from the parameters
|
||||
uv = [
|
||||
@@ -78,7 +73,7 @@ def _tessellate_face(
|
||||
for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1))
|
||||
]
|
||||
|
||||
vertices = [(v.X, v.Y, v.Z) for v in tri_mesh[0]]
|
||||
vertices = tri_mesh[0]
|
||||
indices = tri_mesh[1]
|
||||
mgr.add_face(vertices, indices, uv)
|
||||
|
||||
@@ -91,9 +86,9 @@ def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tupl
|
||||
push_dir = (push_dir[0] + normal.X, push_dir[1] + normal.Y, push_dir[2] + normal.Z)
|
||||
if push_dir != (0, 0, 0):
|
||||
# Normalize the push direction by the number of faces and a constant factor
|
||||
# NOTE: Don't overdo it, or metrics will be wrong
|
||||
n = len(faces) / 1e-3
|
||||
push_dir = (push_dir[0] / n, push_dir[1] / n, push_dir[2] / n)
|
||||
# NOTE: Don't overdo it, or metrics will be (more) wrong
|
||||
n = 1e-3 / len(faces)
|
||||
push_dir = (push_dir[0] * n, push_dir[1] * n, push_dir[2] * n)
|
||||
# Push the vertex by the normal
|
||||
v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2])
|
||||
return v
|
||||
@@ -119,6 +114,9 @@ def _tessellate_edge(
|
||||
for i in range(1, discretizer.NbPoints() + 1)
|
||||
)
|
||||
]
|
||||
|
||||
# Convert strip of vertices to a list of pairs of vertices
|
||||
vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)]
|
||||
mgr.add_edge(vertices)
|
||||
|
||||
|
||||
@@ -127,30 +125,3 @@ def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[Topo
|
||||
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
|
||||
|
||||
|
||||
def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
|
||||
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
|
||||
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
||||
# This is best-effort and not guaranteed to be unique
|
||||
hasher = hashlib.md5(usedforsecurity=False)
|
||||
for k, v in extras.items():
|
||||
hasher.update(str(k).encode())
|
||||
hasher.update(str(v).encode())
|
||||
if isinstance(obj, bytes):
|
||||
hasher.update(obj)
|
||||
elif isinstance(obj, TopLoc_Location):
|
||||
sub_data = io.BytesIO()
|
||||
obj.DumpJson(sub_data)
|
||||
hasher.update(sub_data.getvalue())
|
||||
elif isinstance(obj, TopoDS_Shape):
|
||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||
for i in range(1, map_of_shapes.Extent() + 1):
|
||||
sub_shape = map_of_shapes.FindKey(i)
|
||||
sub_data = io.BytesIO()
|
||||
TopoDS_Shape.DumpJson(sub_shape, sub_data)
|
||||
val = sub_data.getvalue()
|
||||
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
||||
hasher.update(val)
|
||||
else:
|
||||
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
||||
from http.server import ThreadingHTTPServer
|
||||
from importlib.metadata import version
|
||||
from threading import Thread
|
||||
from typing import Optional, Dict, Union, Callable, List
|
||||
from typing import Optional, Dict, Union, Callable, List, Tuple
|
||||
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
@@ -18,11 +18,13 @@ from OCP.TopoDS import TopoDS_Shape
|
||||
from build123d import Shape, Axis, Location, Vector
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
from myhttp import HTTPHandler
|
||||
from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
|
||||
from yacv_server.myhttp import HTTPHandler
|
||||
from yacv_server.mylogger import logger
|
||||
from yacv_server.pubsub import BufferedPubSub
|
||||
from yacv_server.tessellate import _hashcode, tessellate
|
||||
from yacv_server.rwlock import RWLock
|
||||
from yacv_server.tessellate import tessellate
|
||||
from yacv_server.cad import _hashcode
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@@ -33,8 +35,8 @@ class UpdatesApiData:
|
||||
"""Name of the object. Should be unique unless you want to overwrite the previous object"""
|
||||
hash: str
|
||||
"""Hash of the object, to detect changes without rebuilding the object"""
|
||||
is_remove: bool
|
||||
"""Whether to remove the object from the scene"""
|
||||
is_remove: Optional[bool]
|
||||
"""Whether to remove the object from the scene. If None, this is a shutdown request"""
|
||||
|
||||
|
||||
YACVSupported = Union[bytes, CADCoreLike]
|
||||
@@ -46,7 +48,7 @@ class UpdatesApiFullData(UpdatesApiData):
|
||||
kwargs: Optional[Dict[str, any]]
|
||||
"""The show_object options, if any (not serialized)"""
|
||||
|
||||
def __init__(self, obj: YACVSupported, name: str, _hash: str, is_remove: bool = False,
|
||||
def __init__(self, obj: YACVSupported, name: str, _hash: str, is_remove: Optional[bool] = False,
|
||||
kwargs: Optional[Dict[str, any]] = None):
|
||||
self.name = name
|
||||
self.hash = _hash
|
||||
@@ -60,22 +62,42 @@ class UpdatesApiFullData(UpdatesApiData):
|
||||
|
||||
|
||||
class YACV:
|
||||
"""The main yacv_server class, which manages the web server and the CAD objects."""
|
||||
|
||||
# Startup
|
||||
server_thread: Optional[Thread]
|
||||
"""The main thread running the server (will spawn other threads for each request)"""
|
||||
server: Optional[ThreadingHTTPServer]
|
||||
"""The server object"""
|
||||
startup_complete: threading.Event
|
||||
"""Event to signal when the server has started"""
|
||||
|
||||
# Running
|
||||
show_events: BufferedPubSub[UpdatesApiFullData]
|
||||
"""PubSub for show events (objects to be shown in/removed from the scene)"""
|
||||
build_events: Dict[str, BufferedPubSub[bytes]]
|
||||
object_events_lock: threading.Lock
|
||||
"""PubSub for build events (objects that were built)"""
|
||||
build_events_lock: threading.Lock
|
||||
"""Lock to ensure that objects are only built once"""
|
||||
|
||||
# Shutdown
|
||||
at_least_one_client: threading.Event
|
||||
"""Event to signal when at least one client has connected"""
|
||||
shutting_down: threading.Event
|
||||
"""Event to signal when the server is shutting down"""
|
||||
frontend_lock: RWLock
|
||||
"""Lock to ensure that the frontend has finished working before we shut down"""
|
||||
|
||||
def __init__(self):
|
||||
self.server_thread = None
|
||||
self.server = None
|
||||
self.startup_complete = threading.Event()
|
||||
self.at_least_one_client = threading.Event()
|
||||
self.show_events = BufferedPubSub()
|
||||
self.build_events = {}
|
||||
self.object_events_lock = threading.Lock()
|
||||
self.frontend_lock = threading.Lock()
|
||||
self.build_events_lock = threading.Lock()
|
||||
self.at_least_one_client = threading.Event()
|
||||
self.shutting_down = threading.Event()
|
||||
self.frontend_lock = RWLock()
|
||||
logger.info('Using yacv-server v%s', version('yacv-server'))
|
||||
|
||||
def start(self):
|
||||
@@ -100,38 +122,36 @@ class YACV:
|
||||
logger.error('Cannot stop server because it is not running')
|
||||
return
|
||||
|
||||
# Inform the server that we are shutting down
|
||||
self.shutting_down.set()
|
||||
# noinspection PyTypeChecker
|
||||
self.show_events.publish(UpdatesApiFullData(name='__shutdown', _hash='', is_remove=None, obj=None))
|
||||
|
||||
# If we were too fast, ensure that at least one client has connected
|
||||
graceful_secs_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0))
|
||||
graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_REQUEST', 5.0))
|
||||
# Make sure we can hold the lock for more than 100ms (to avoid exiting too early)
|
||||
logger.info('Stopping server (waiting for at least one frontend request first, cancel with CTRL+C)...')
|
||||
start = time.time()
|
||||
try:
|
||||
while not self.at_least_one_client.wait(
|
||||
graceful_secs_connect / 10) and time.time() - start < graceful_secs_connect:
|
||||
time.sleep(0.01)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
if graceful_secs_connect > 0:
|
||||
start = time.time()
|
||||
try:
|
||||
if not self.at_least_one_client.is_set():
|
||||
logger.warning(
|
||||
'Waiting for at least one frontend request before stopping server, cancel with CTRL+C...')
|
||||
while (not self.at_least_one_client.wait(graceful_secs_connect / 10) and
|
||||
time.time() - start < graceful_secs_connect):
|
||||
time.sleep(0.01)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
logger.info('Stopping server (waiting for no more frontend requests)...')
|
||||
start = time.time()
|
||||
try:
|
||||
while time.time() - start < graceful_secs_request:
|
||||
if self.frontend_lock.locked():
|
||||
start = time.time()
|
||||
time.sleep(0.01)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
# Wait for the server to stop gracefully (all frontends to stop working)
|
||||
graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_WORK', 1000000))
|
||||
with self.frontend_lock.w_locked(timeout=graceful_secs_request):
|
||||
# Stop the server
|
||||
self.server.shutdown()
|
||||
|
||||
# Stop the server in the background
|
||||
self.server.shutdown()
|
||||
logger.info('Stopping server (sent)...')
|
||||
|
||||
# Wait for the server to stop gracefully
|
||||
self.server_thread.join(timeout=30)
|
||||
self.server_thread = None
|
||||
logger.info('Stopping server (confirmed)...')
|
||||
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
|
||||
sys.exit(0) # Exit with success
|
||||
# Wait for the server thread to stop
|
||||
self.server_thread.join(timeout=30)
|
||||
self.server_thread = None
|
||||
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
|
||||
sys.exit(0) # Exit with success
|
||||
|
||||
def _run_server(self):
|
||||
"""Runs the web server"""
|
||||
@@ -175,7 +195,7 @@ class YACV:
|
||||
|
||||
def show_cad_all(self, **kwargs):
|
||||
"""Publishes all CAD objects in the current scope to the server"""
|
||||
all_cad = grab_all_cad()
|
||||
all_cad = list(grab_all_cad()) # List for reproducible iteration order
|
||||
self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
|
||||
|
||||
def remove(self, name: str):
|
||||
@@ -187,7 +207,7 @@ class YACV:
|
||||
self.show_events.delete(old_show_event)
|
||||
|
||||
# Delete any cached object builds
|
||||
with self.object_events_lock:
|
||||
with self.build_events_lock:
|
||||
if name in self.build_events:
|
||||
del self.build_events[name]
|
||||
|
||||
@@ -206,13 +226,13 @@ class YACV:
|
||||
|
||||
def shown_object_names(self, apply_removes: bool = True) -> List[str]:
|
||||
"""Returns the names of all objects that have been shown"""
|
||||
res = []
|
||||
res = set()
|
||||
for obj in self.show_events.buffer():
|
||||
if not obj.is_remove or not apply_removes:
|
||||
res.append(obj.name)
|
||||
res.add(obj.name)
|
||||
else:
|
||||
res.remove(obj.name)
|
||||
return res
|
||||
res.discard(obj.name)
|
||||
return list(res)
|
||||
|
||||
def _show_events(self, name: str, apply_removes: bool = True) -> List[UpdatesApiFullData]:
|
||||
"""Returns the show events with the given name"""
|
||||
@@ -228,8 +248,8 @@ class YACV:
|
||||
res.remove(old_event)
|
||||
return res
|
||||
|
||||
def export(self, name: str) -> Optional[bytes]:
|
||||
"""Export the given previously-shown object to a single GLB file, building it if necessary."""
|
||||
def export(self, name: str) -> Optional[Tuple[bytes, str]]:
|
||||
"""Export the given previously-shown object to a single GLB blob, building it if necessary."""
|
||||
start = time.time()
|
||||
|
||||
# Check that the object to build exists and grab it if it does
|
||||
@@ -240,7 +260,7 @@ class YACV:
|
||||
event = events[-1]
|
||||
|
||||
# Use the lock to ensure that we don't build the object twice
|
||||
with self.object_events_lock:
|
||||
with self.build_events_lock:
|
||||
# If there are no object events for this name, we need to build the object
|
||||
if name not in self.build_events:
|
||||
logger.debug('Building object %s with hash %s', name, event.hash)
|
||||
@@ -259,14 +279,15 @@ class YACV:
|
||||
edges=event.kwargs.get('edges', True),
|
||||
vertices=event.kwargs.get('vertices', True))
|
||||
glb_list_of_bytes = gltf.save_to_bytes()
|
||||
publish_to.publish(b''.join(glb_list_of_bytes))
|
||||
logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start,
|
||||
len(gltf.meshes[0].primitives))
|
||||
glb_bytes = b''.join(glb_list_of_bytes)
|
||||
publish_to.publish(glb_bytes)
|
||||
logger.info('export(%s) took %.3f seconds, %s', name, time.time() - start,
|
||||
sizeof_fmt(len(glb_bytes)))
|
||||
|
||||
# In either case return the elements of a subscription to the async generator
|
||||
subscription = self.build_events[name].subscribe()
|
||||
try:
|
||||
return next(subscription)
|
||||
return next(subscription), event.hash
|
||||
finally:
|
||||
subscription.close()
|
||||
|
||||
@@ -277,7 +298,7 @@ class YACV:
|
||||
for name in self.shown_object_names():
|
||||
if export_filter(name, self._show_events(name)[-1].obj):
|
||||
with open(os.path.join(folder, f'{name}.glb'), 'wb') as f:
|
||||
f.write(self.export(name))
|
||||
f.write(self.export(name)[0])
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@@ -302,12 +323,21 @@ def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
|
||||
_find_var_name_count = 0
|
||||
|
||||
|
||||
def _find_var_name(obj: any) -> str:
|
||||
def _find_var_name(obj: any, avoid_levels: int = 2) -> str:
|
||||
"""A hacky way to get a stable name for an object that may change over time"""
|
||||
global _find_var_name_count
|
||||
for frame in inspect.stack():
|
||||
obj_shape = get_shape(obj, error=False) or obj
|
||||
for frame in inspect.stack()[avoid_levels:]:
|
||||
for key, value in frame.frame.f_locals.items():
|
||||
if value is obj:
|
||||
if get_shape(value, error=False) is obj_shape:
|
||||
return key
|
||||
_find_var_name_count += 1
|
||||
return 'unknown_var_' + str(_find_var_name_count)
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f}{unit}{suffix}"
|
||||
num /= 1024.0
|
||||
return f"{num:.1f}Yi{suffix}"
|
||||
|
||||
530
yarn.lock
530
yarn.lock
@@ -275,143 +275,143 @@
|
||||
resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz#dd19fcb8cc6e33ede237fc1b7af96c70833d8f93"
|
||||
integrity sha512-GuvZ38d23H+7Tz2C9DhzCepivsOsky03s5NI+KCy7ke1FNUvsJ2oO47scQ9YaGGhgjgNW5OYYNSADmbjcSoIhw==
|
||||
|
||||
"@esbuild/aix-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
|
||||
integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
|
||||
"@esbuild/aix-ppc64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
||||
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
|
||||
|
||||
"@esbuild/android-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
|
||||
integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
|
||||
"@esbuild/android-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
|
||||
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
|
||||
|
||||
"@esbuild/android-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
|
||||
integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
|
||||
"@esbuild/android-arm@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
|
||||
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
|
||||
|
||||
"@esbuild/android-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
|
||||
integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
|
||||
"@esbuild/android-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
|
||||
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
|
||||
|
||||
"@esbuild/darwin-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
|
||||
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
|
||||
"@esbuild/darwin-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
|
||||
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
|
||||
|
||||
"@esbuild/darwin-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
|
||||
integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
|
||||
"@esbuild/darwin-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
|
||||
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
|
||||
integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
|
||||
"@esbuild/freebsd-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
|
||||
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
|
||||
integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
|
||||
"@esbuild/freebsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
|
||||
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
|
||||
|
||||
"@esbuild/linux-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
|
||||
integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
|
||||
"@esbuild/linux-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
|
||||
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
|
||||
|
||||
"@esbuild/linux-arm@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
|
||||
integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
|
||||
"@esbuild/linux-arm@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
|
||||
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
|
||||
|
||||
"@esbuild/linux-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
|
||||
integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
|
||||
"@esbuild/linux-ia32@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
|
||||
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
|
||||
|
||||
"@esbuild/linux-loong64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
|
||||
integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
|
||||
"@esbuild/linux-loong64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
|
||||
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
|
||||
|
||||
"@esbuild/linux-mips64el@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
|
||||
integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
|
||||
"@esbuild/linux-mips64el@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
|
||||
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
|
||||
integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
|
||||
"@esbuild/linux-ppc64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
|
||||
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
|
||||
|
||||
"@esbuild/linux-riscv64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
|
||||
integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
|
||||
"@esbuild/linux-riscv64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
|
||||
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
|
||||
|
||||
"@esbuild/linux-s390x@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
|
||||
integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
|
||||
"@esbuild/linux-s390x@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
|
||||
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
|
||||
|
||||
"@esbuild/linux-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
|
||||
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
|
||||
"@esbuild/linux-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
|
||||
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
|
||||
|
||||
"@esbuild/netbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
|
||||
integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
|
||||
"@esbuild/netbsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
|
||||
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
|
||||
integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
|
||||
"@esbuild/openbsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
|
||||
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
|
||||
|
||||
"@esbuild/sunos-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
|
||||
integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
|
||||
"@esbuild/sunos-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
|
||||
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
|
||||
|
||||
"@esbuild/win32-arm64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
|
||||
integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
|
||||
"@esbuild/win32-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
|
||||
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
|
||||
integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
|
||||
"@esbuild/win32-ia32@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
|
||||
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
|
||||
|
||||
"@esbuild/win32-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
|
||||
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
|
||||
"@esbuild/win32-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||
|
||||
"@gltf-transform/core@^3.10.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.0.tgz#854e7345f23971e4e7367a29183a2d1b62d45e46"
|
||||
integrity sha512-NxVKhSWvH0j1tjZE8Yl461HUMyZLmYmqcbqHw0TOcQd5Q1SV7Y5w6W68XMt9/amRfMAiJLLNREE7kbr+Z0Ydbw==
|
||||
"@gltf-transform/core@^3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
||||
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
||||
dependencies:
|
||||
property-graph "^1.3.1"
|
||||
|
||||
"@gltf-transform/extensions@^3.10.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/extensions/-/extensions-3.10.0.tgz#4ae11c3fe8e2a77e6e9dd04ebf0931c7b0cd3690"
|
||||
integrity sha512-dz/cf2toBzP+w3ES2VgMiINCN6q86MVGu1lHkT0El4No77Bje9fnHVEPrKwaDCsXi5YXUiG/u6686vK6jePwDA==
|
||||
"@gltf-transform/extensions@^3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/extensions/-/extensions-3.10.1.tgz#71664389cae46fb12eb97dc71eb96d86a0d7801f"
|
||||
integrity sha512-xUS9K5fMvW2dkYN4VzxHg2aBPG54M2WqgIjQ7RoSyybMoD7DsPUyMyVgRja+aiTVt/Bxza2ve7zJBD3+tN+aTA==
|
||||
dependencies:
|
||||
"@gltf-transform/core" "^3.10.0"
|
||||
"@gltf-transform/core" "^3.10.1"
|
||||
ktx-parse "^0.6.0"
|
||||
|
||||
"@gltf-transform/functions@^3.10.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/functions/-/functions-3.10.0.tgz#bf0331c109ac948d19be7394d3afcfae84215cfd"
|
||||
integrity sha512-FStbDaH7t2z74RyEeUQn3aBcybULbDkt72ZasC0s7DwQ2DFKKKOth4Zksi4g9+8URNM6vNa2JSfuO851dkJHEg==
|
||||
"@gltf-transform/functions@^3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/functions/-/functions-3.10.1.tgz#c40817740241c0ee770f4d1210ccc766e46d8ab2"
|
||||
integrity sha512-Zs6+1qvTD9w40R5qv70E4wJXXacNQ46ZxjKKW6dmfGIyjT8bsSJmV3Tdj+WJ8R6lWXXZ8e2p3ZvAUfPDEG73bQ==
|
||||
dependencies:
|
||||
"@gltf-transform/core" "^3.10.0"
|
||||
"@gltf-transform/extensions" "^3.10.0"
|
||||
"@gltf-transform/core" "^3.10.1"
|
||||
"@gltf-transform/extensions" "^3.10.1"
|
||||
ktx-parse "^0.6.0"
|
||||
ndarray "^1.0.19"
|
||||
ndarray-lanczos "^0.3.0"
|
||||
@@ -668,70 +668,70 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6"
|
||||
integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==
|
||||
"@rollup/rollup-android-arm-eabi@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b"
|
||||
integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2"
|
||||
integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==
|
||||
"@rollup/rollup-android-arm64@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e"
|
||||
integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69"
|
||||
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==
|
||||
"@rollup/rollup-darwin-arm64@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b"
|
||||
integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8"
|
||||
integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==
|
||||
"@rollup/rollup-darwin-x64@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455"
|
||||
integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d"
|
||||
integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4"
|
||||
integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68"
|
||||
integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==
|
||||
"@rollup/rollup-linux-arm64-gnu@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143"
|
||||
integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7"
|
||||
integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==
|
||||
"@rollup/rollup-linux-arm64-musl@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef"
|
||||
integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7"
|
||||
integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6"
|
||||
integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3"
|
||||
integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==
|
||||
"@rollup/rollup-linux-x64-gnu@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778"
|
||||
integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05"
|
||||
integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==
|
||||
"@rollup/rollup-linux-x64-musl@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89"
|
||||
integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e"
|
||||
integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==
|
||||
"@rollup/rollup-win32-arm64-msvc@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b"
|
||||
integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40"
|
||||
integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==
|
||||
"@rollup/rollup-win32-ia32-msvc@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2"
|
||||
integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235"
|
||||
integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==
|
||||
"@rollup/rollup-win32-x64-msvc@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10"
|
||||
integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==
|
||||
|
||||
"@sigstore/bundle@^2.2.0":
|
||||
version "2.2.0"
|
||||
@@ -777,10 +777,10 @@
|
||||
"@sigstore/core" "^1.0.0"
|
||||
"@sigstore/protobuf-specs" "^0.3.0"
|
||||
|
||||
"@tsconfig/node20@^20.1.2":
|
||||
version "20.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.2.tgz#b93128c411d38e9507035255195bc8a6718541e3"
|
||||
integrity sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==
|
||||
"@tsconfig/node20@^20.1.4":
|
||||
version "20.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||
|
||||
"@tufjs/canonical-json@2.0.0":
|
||||
version "2.0.0"
|
||||
@@ -805,10 +805,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
||||
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
||||
|
||||
"@types/node@^20.11.25":
|
||||
version "20.11.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.25.tgz#0f50d62f274e54dd7a49f7704cc16bfbcccaf49f"
|
||||
integrity sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==
|
||||
"@types/node@^20.12.2":
|
||||
version "20.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
||||
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -851,26 +851,26 @@
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37"
|
||||
integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==
|
||||
|
||||
"@volar/language-core@2.1.2", "@volar/language-core@~2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.2.tgz#2053c0ee48a822d5418be2c192e51e580764b49f"
|
||||
integrity sha512-5qsDp0Gf6fE09UWCeK7bkVn6NxMwC9OqFWQkMMkeej8h8XjyABPdRygC2RCrqDrfVdGijqlMQeXs6yRS+vfZYA==
|
||||
"@volar/language-core@2.1.5", "@volar/language-core@~2.1.3":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.5.tgz#78803531562ca423a3b4fa1b3a90d33537aab928"
|
||||
integrity sha512-u1OHmVkCFsJqNdaM2GKuMhE67TxcEnOqJNF+VtYv2Ji8DnrUaF4FAFSNxY+MRGICl+873CsSJVKas9TQtW14LA==
|
||||
dependencies:
|
||||
"@volar/source-map" "2.1.2"
|
||||
"@volar/source-map" "2.1.5"
|
||||
|
||||
"@volar/source-map@2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.2.tgz#d270ff8ef5c814582f0efe08272c0fd6b9effb3b"
|
||||
integrity sha512-yFJqsuLm1OaWrsz9E3yd3bJcYIlHqdZ8MbmIoZLrAzMYQDcoF26/INIhgziEXSdyHc8xd7rd/tJdSnUyh0gH4Q==
|
||||
"@volar/source-map@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.5.tgz#c073549368d399365f0024ed89aea93de0807328"
|
||||
integrity sha512-GIkAM6fHgDcTXcdH4i10fAiAZzO0HLIer8/pt3oZ9A0n7n4R5d1b2F8Xxzh/pgmgNoL+SrHX3MFxs35CKgfmtA==
|
||||
dependencies:
|
||||
muggle-string "^0.4.0"
|
||||
|
||||
"@volar/typescript@~2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.2.tgz#61f838cf4410e328a7ba638fadc41bb814772508"
|
||||
integrity sha512-lhTancZqamvaLvoz0u/uth8dpudENNt2LFZOWCw9JZiX14xRFhdhfzmphiCRb7am9E6qAJSbdS/gMt1utXAoHQ==
|
||||
"@volar/typescript@~2.1.3":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.5.tgz#756a8626af1c5ce3c9d8df3e79d49bfdd0adf39e"
|
||||
integrity sha512-zo9a3NrNMSkufIvHuExDGTfYv+zO7C5p2wg8fyP7vcqF/Qo0ztjb0ZfOgq/A85EO/MBc1Kj2Iu7PaOBtP++NMw==
|
||||
dependencies:
|
||||
"@volar/language-core" "2.1.2"
|
||||
"@volar/language-core" "2.1.5"
|
||||
path-browserify "^1.0.1"
|
||||
|
||||
"@vue/babel-helper-vue-transform-on@1.2.1":
|
||||
@@ -948,12 +948,12 @@
|
||||
"@vue/compiler-dom" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
"@vue/language-core@2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.6.tgz#876f90622a3f801dce5cedcd6eae429d732152e2"
|
||||
integrity sha512-UzqU12tzf9XLqRO3TiWPwRNpP4fyUzE6MAfOQWQNZ4jy6a30ARRUpmODDKq6O8C4goMc2AlPqTmjOHPjHkilSg==
|
||||
"@vue/language-core@2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.7.tgz#af12f752a93c4d2498626fca33f5d1ddc8c5ceb9"
|
||||
integrity sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==
|
||||
dependencies:
|
||||
"@volar/language-core" "~2.1.2"
|
||||
"@volar/language-core" "~2.1.3"
|
||||
"@vue/compiler-dom" "^3.4.0"
|
||||
"@vue/shared" "^3.4.0"
|
||||
computeds "^0.0.1"
|
||||
@@ -1481,34 +1481,34 @@ error-ex@^1.3.1:
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
esbuild@^0.19.3:
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
|
||||
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
|
||||
esbuild@^0.20.1:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
|
||||
integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.19.12"
|
||||
"@esbuild/android-arm" "0.19.12"
|
||||
"@esbuild/android-arm64" "0.19.12"
|
||||
"@esbuild/android-x64" "0.19.12"
|
||||
"@esbuild/darwin-arm64" "0.19.12"
|
||||
"@esbuild/darwin-x64" "0.19.12"
|
||||
"@esbuild/freebsd-arm64" "0.19.12"
|
||||
"@esbuild/freebsd-x64" "0.19.12"
|
||||
"@esbuild/linux-arm" "0.19.12"
|
||||
"@esbuild/linux-arm64" "0.19.12"
|
||||
"@esbuild/linux-ia32" "0.19.12"
|
||||
"@esbuild/linux-loong64" "0.19.12"
|
||||
"@esbuild/linux-mips64el" "0.19.12"
|
||||
"@esbuild/linux-ppc64" "0.19.12"
|
||||
"@esbuild/linux-riscv64" "0.19.12"
|
||||
"@esbuild/linux-s390x" "0.19.12"
|
||||
"@esbuild/linux-x64" "0.19.12"
|
||||
"@esbuild/netbsd-x64" "0.19.12"
|
||||
"@esbuild/openbsd-x64" "0.19.12"
|
||||
"@esbuild/sunos-x64" "0.19.12"
|
||||
"@esbuild/win32-arm64" "0.19.12"
|
||||
"@esbuild/win32-ia32" "0.19.12"
|
||||
"@esbuild/win32-x64" "0.19.12"
|
||||
"@esbuild/aix-ppc64" "0.20.2"
|
||||
"@esbuild/android-arm" "0.20.2"
|
||||
"@esbuild/android-arm64" "0.20.2"
|
||||
"@esbuild/android-x64" "0.20.2"
|
||||
"@esbuild/darwin-arm64" "0.20.2"
|
||||
"@esbuild/darwin-x64" "0.20.2"
|
||||
"@esbuild/freebsd-arm64" "0.20.2"
|
||||
"@esbuild/freebsd-x64" "0.20.2"
|
||||
"@esbuild/linux-arm" "0.20.2"
|
||||
"@esbuild/linux-arm64" "0.20.2"
|
||||
"@esbuild/linux-ia32" "0.20.2"
|
||||
"@esbuild/linux-loong64" "0.20.2"
|
||||
"@esbuild/linux-mips64el" "0.20.2"
|
||||
"@esbuild/linux-ppc64" "0.20.2"
|
||||
"@esbuild/linux-riscv64" "0.20.2"
|
||||
"@esbuild/linux-s390x" "0.20.2"
|
||||
"@esbuild/linux-x64" "0.20.2"
|
||||
"@esbuild/netbsd-x64" "0.20.2"
|
||||
"@esbuild/openbsd-x64" "0.20.2"
|
||||
"@esbuild/sunos-x64" "0.20.2"
|
||||
"@esbuild/win32-arm64" "0.20.2"
|
||||
"@esbuild/win32-ia32" "0.20.2"
|
||||
"@esbuild/win32-x64" "0.20.2"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.2"
|
||||
@@ -2410,14 +2410,14 @@ postcss-selector-parser@^6.0.10:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.4.35:
|
||||
version "8.4.35"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7"
|
||||
integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==
|
||||
postcss@^8.4.35, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
prebuild-install@^7.1.1:
|
||||
version "7.1.2"
|
||||
@@ -2551,26 +2551,26 @@ retry@^0.12.0:
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
|
||||
|
||||
rollup@^4.2.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5"
|
||||
integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==
|
||||
rollup@^4.13.0:
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a"
|
||||
integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.5"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.12.0"
|
||||
"@rollup/rollup-android-arm64" "4.12.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.12.0"
|
||||
"@rollup/rollup-darwin-x64" "4.12.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.12.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.12.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.12.0"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.12.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.12.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.12.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.12.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.12.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.12.0"
|
||||
"@rollup/rollup-android-arm-eabi" "4.13.0"
|
||||
"@rollup/rollup-android-arm64" "4.13.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.13.0"
|
||||
"@rollup/rollup-darwin-x64" "4.13.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.13.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.13.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.13.0"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.13.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.13.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.13.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.13.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.13.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.13.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
@@ -2701,6 +2701,11 @@ source-map-js@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
source-map-js@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
@@ -2878,16 +2883,21 @@ tar@^6.1.11, tar@^6.1.2:
|
||||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
terser@^5.29.1:
|
||||
version "5.29.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.1.tgz#44e58045b70c09792ba14bfb7b4e14ca8755b9fa"
|
||||
integrity sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==
|
||||
terser@^5.30.0:
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
|
||||
integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
three-mesh-bvh@^0.7.3:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.3.tgz#91f2d7e26f230288b5b0a6bdf41bdd9620348945"
|
||||
integrity sha512-3W6KjzmupjfE89GuHPT31kxKWZ4YGZPEZJNysJpiOZfQRsBQQgmK7v/VJPpjG6syhAvTnY+5Fr77EvIkTLpGSw==
|
||||
|
||||
"three-orientation-gizmo@https://github.com/jrj2211/three-orientation-gizmo":
|
||||
version "1.1.0"
|
||||
resolved "https://github.com/jrj2211/three-orientation-gizmo#000281f0559c316f72cdd23a1885d63ae6901095"
|
||||
@@ -2935,10 +2945,10 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
typescript@~5.4.2:
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
|
||||
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
|
||||
typescript@~5.4.3:
|
||||
version "5.4.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff"
|
||||
integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
@@ -2992,14 +3002,14 @@ validate-npm-package-name@^5.0.0:
|
||||
dependencies:
|
||||
builtins "^5.0.0"
|
||||
|
||||
vite@^5.1.5:
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.5.tgz#bdbc2b15e8000d9cc5172f059201178f9c9de5fb"
|
||||
integrity sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==
|
||||
vite@^5.2.7:
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
|
||||
integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
|
||||
dependencies:
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.35"
|
||||
rollup "^4.2.0"
|
||||
esbuild "^0.20.1"
|
||||
postcss "^8.4.38"
|
||||
rollup "^4.13.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
@@ -3011,13 +3021,13 @@ vue-template-compiler@^2.7.14:
|
||||
de-indent "^1.0.2"
|
||||
he "^1.2.0"
|
||||
|
||||
vue-tsc@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.6.tgz#29cb195ffea63d66ec70b64eb4aadc6cd162bb12"
|
||||
integrity sha512-kK50W4XqQL34vHRkxlRWLicrT6+F9xfgCgJ4KSmCHcytKzc1u3c94XXgI+CjmhOSxyw0krpExF7Obo7y4+0dVQ==
|
||||
vue-tsc@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.7.tgz#3177a2fe720bfa7355d3717929ee8c8d132bc5d0"
|
||||
integrity sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==
|
||||
dependencies:
|
||||
"@volar/typescript" "~2.1.2"
|
||||
"@vue/language-core" "2.0.6"
|
||||
"@volar/typescript" "~2.1.3"
|
||||
"@vue/language-core" "2.0.7"
|
||||
semver "^7.5.4"
|
||||
|
||||
vue@^3.4.21:
|
||||
@@ -3031,10 +3041,10 @@ vue@^3.4.21:
|
||||
"@vue/server-renderer" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
vuetify@^3.5.8:
|
||||
version "3.5.8"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.8.tgz#bc8f08dfd3314640e7b5d43b50138a26d650cbbf"
|
||||
integrity sha512-8nGS+lKejZkev55HFwIfsRt+9fOqbeDQNmXxfmLKAlnUT8FtynVwbjAwHMtX/OQAQ3ZwRaR1ptqQQmx3OgxzbQ==
|
||||
vuetify@^3.5.13:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
|
||||
integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
|
||||
|
||||
walk-up-path@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
||||
Reference in New Issue
Block a user