Compare commits

...

70 Commits

Author SHA1 Message Date
Yeicor
27f540be23 Automatically update version to 0.8.8 2024-03-31 18:24:32 +00:00
Yeicor
e8c0f683c5 Lots of frontend improvements like keeping camera position on changes, avoid flickering on live updates, smooth gizmo animations, proper ortho camera movement, and enabling move by tap when not selecting. 2024-03-31 20:20:53 +02:00
Yeicor
345636e478 Automatically update version to 0.8.7 2024-03-30 17:02:20 +00:00
Yeicor
9a0fb03526 Merge remote-tracking branch 'origin/master' 2024-03-30 18:00:50 +01:00
Yeicor
2037621afc Fix frontend style issues 2024-03-30 18:00:37 +01:00
Yeicor
2ba0e18479 Automatically update version to 0.8.6 2024-03-30 16:31:40 +00:00
Yeicor
eca2bbfa7c Fix python import bug 2024-03-30 17:28:49 +01:00
Yeicor
86180c424e Keep selected enabled features on model updates instead of resetting them, better list of objects support and recover/disable previous selection on scene reloads. 2024-03-30 17:26:06 +01:00
dependabot[bot]
e42d6be074 Bump @tsconfig/node20 from 20.1.3 to 20.1.4 (#30)
Bumps [@tsconfig/node20](https://github.com/tsconfig/bases/tree/HEAD/bases) from 20.1.3 to 20.1.4.
- [Commits](https://github.com/tsconfig/bases/commits/HEAD/bases)

---
updated-dependencies:
- dependency-name: "@tsconfig/node20"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 10:57:52 +00:00
dependabot[bot]
e2d6a3cb00 Bump actions/configure-pages from 4 to 5 in /.github/workflows (#31)
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 4 to 5.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 09:15:41 +00:00
dependabot[bot]
9e453b7890 Bump vuetify from 3.5.11 to 3.5.13 (#29)
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.5.11 to 3.5.13.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.5.13/packages/vuetify)

---
updated-dependencies:
- dependency-name: vuetify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 09:14:06 +00:00
dependabot[bot]
0b8faa9e8b Bump vite from 5.2.6 to 5.2.7 (#28)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.6 to 5.2.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 09:13:56 +00:00
dependabot[bot]
00bc2a15e0 Bump @types/node from 20.11.30 to 20.12.2 (#27)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.30 to 20.12.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 09:13:29 +00:00
dependabot[bot]
432abcf85c Bump terser from 5.29.2 to 5.30.0 (#26)
Bumps [terser](https://github.com/terser/terser) from 5.29.2 to 5.30.0.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.29.2...v5.30.0)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-30 09:13:17 +00:00
Yeicor
4b6d3f6266 Automatically update version to 0.8.5 2024-03-29 11:26:06 +00:00
Yeicor
255ae72ed2 Count features again after changes to model and support for sending arbitrary lists of shapes as a single model. 2024-03-29 12:23:16 +01:00
Yeicor
77dd9fb43e Merge remote-tracking branch 'origin/master' 2024-03-28 23:32:28 +01:00
Yeicor
5dc2ae2f8d Remove debug check 2024-03-28 23:32:20 +01:00
Yeicor
58440723bd Automatically update version to 0.8.4 2024-03-28 22:29:24 +00:00
Yeicor
bfdd656316 Merge remote-tracking branch 'origin/master' 2024-03-28 23:28:43 +01:00
Yeicor
7408823c02 Debug CI 2024-03-28 23:28:36 +01:00
Yeicor
856ffbc4c5 Reduce logging 2024-03-28 23:26:00 +01:00
Yeicor
d0f8463bbf Automatically update version to 0.8.3 2024-03-28 22:01:12 +00:00
Yeicor
162d3e22a2 Fix typescript 2024-03-28 22:57:26 +01:00
Yeicor
4b06559ab8 Add a progress bar to the frontend and improve slightly batched updates logic 2024-03-28 22:52:34 +01:00
Yeicor
9afa2e5786 Add support for some gltf extensions and better multi-object updates 2024-03-28 19:12:21 +01:00
Yeicor
7196fb2f32 Automatically update version to 0.8.2 2024-03-28 11:52:24 +00:00
Yeicor
8ec60faa04 Merge remote-tracking branch 'origin/master' 2024-03-28 12:51:34 +01:00
Yeicor
13bbdd5956 Fix automatic _find_var_name 2024-03-28 12:51:26 +01:00
Yeicor
3675d2f447 Automatically update version to 0.8.1 2024-03-28 11:23:32 +00:00
Yeicor
efc7a1d3b6 Merge remote-tracking branch 'origin/master' 2024-03-28 12:21:10 +01:00
Yeicor
7166f9fe3d Improved image location after build123d update 2024-03-28 12:20:56 +01:00
Yeicor
3405de38e7 Automatically update version to 0.8.0 2024-03-27 19:18:02 +00:00
Yeicor
2bd927f2a8 Updates 2 2024-03-27 20:17:19 +01:00
Yeicor
9718172fdd Updates 2024-03-27 20:16:49 +01:00
Yeicor
472a7a8309 Merge remote-tracking branch 'origin/master' 2024-03-26 22:02:40 +01:00
Yeicor
7a7627f57e clean code 2024-03-26 22:02:32 +01:00
Yeicor
064d9aeb35 Automatically update version to 0.7.1 2024-03-26 20:30:21 +00:00
Yeicor
eed0baccac fix automatic naming of objects 2024-03-26 21:25:28 +01:00
Yeicor
72480d82c8 strong performance optimizations for the backend 2024-03-26 21:22:48 +01:00
Yeicor
3de710c8b5 Merge remote-tracking branch 'origin/master' 2024-03-26 20:43:31 +01:00
Yeicor
8ebf48cb36 configurable edge and vertex widths 2024-03-26 20:43:21 +01:00
Yeicor
5083037b73 Automatically update version to 0.7.0 2024-03-25 20:39:40 +00:00
Yeicor
632e7e93c6 lots of performance improvements, bug fixes and some new features 2024-03-25 21:37:28 +01:00
dependabot[bot]
ec7139c809 Bump vite from 5.1.6 to 5.2.3 (#23)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.6 to 5.2.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@5.2.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:58:13 +00:00
dependabot[bot]
752d6dc778 Bump vuetify from 3.5.9 to 3.5.11 (#25)
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.5.9 to 3.5.11.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.5.11/packages/vuetify)

---
updated-dependencies:
- dependency-name: vuetify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:57:06 +00:00
dependabot[bot]
6d60b7b003 Bump typescript from 5.4.2 to 5.4.3 (#24)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.4.2...v5.4.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:56:54 +00:00
dependabot[bot]
675e2e970a Bump @types/node from 20.11.28 to 20.11.30 (#22)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.28 to 20.11.30.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:56:21 +00:00
dependabot[bot]
70bef528c5 Bump vue-tsc from 2.0.6 to 2.0.7 (#21)
Bumps [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v2.0.7/packages/tsc)

---
updated-dependencies:
- dependency-name: vue-tsc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:56:08 +00:00
dependabot[bot]
78319fead5 Bump dependabot/fetch-metadata from 1 to 2 in /.github/workflows (#20)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1 to 2.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1...v2)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-23 09:23:39 +00:00
Yeicor
6944f69110 Automatically update version to 0.6.18 2024-03-16 15:23:23 +00:00
Yeicor
1d01c75448 more fixes 2024-03-16 16:22:08 +01:00
Yeicor
cb0a7bdf0c Automatically update version to 0.6.17 2024-03-16 09:57:27 +00:00
Yeicor
a7dba6fd1b Merge remote-tracking branch 'origin/master' 2024-03-16 10:56:54 +01:00
Yeicor
981d923e5e fix 2024-03-16 10:56:46 +01:00
Yeicor
6f95a2f3ad Automatically update version to 0.6.16 2024-03-16 09:55:30 +00:00
Yeicor
63c74461b2 Merge remote-tracking branch 'origin/master' 2024-03-16 10:54:38 +01:00
Yeicor
e85dc36fea clean frontend disconnection protocol 2024-03-16 10:54:26 +01:00
dependabot[bot]
43d30b0fdd Bump vite from 5.1.5 to 5.1.6 (#19)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.5 to 5.1.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 09:18:16 +00:00
dependabot[bot]
acba91322c Bump terser from 5.29.1 to 5.29.2 (#18)
Bumps [terser](https://github.com/terser/terser) from 5.29.1 to 5.29.2.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.29.1...v5.29.2)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 09:18:03 +00:00
dependabot[bot]
ba7ce3727d Bump @types/node from 20.11.25 to 20.11.28 (#16)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.25 to 20.11.28.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 09:17:45 +00:00
dependabot[bot]
d168806744 Bump vuetify from 3.5.8 to 3.5.9 (#15)
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.5.8 to 3.5.9.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.5.9/packages/vuetify)

---
updated-dependencies:
- dependency-name: vuetify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 09:17:27 +00:00
dependabot[bot]
919c05eb9d Bump @gltf-transform/functions from 3.10.0 to 3.10.1 (#14)
Bumps [@gltf-transform/functions](https://github.com/donmccurdy/glTF-Transform) from 3.10.0 to 3.10.1.
- [Changelog](https://github.com/donmccurdy/glTF-Transform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/donmccurdy/glTF-Transform/compare/v3.10.0...v3.10.1)

---
updated-dependencies:
- dependency-name: "@gltf-transform/functions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 09:17:17 +00:00
Yeicor
2370fd72ed Automatically update version to 0.6.15 2024-03-14 16:27:55 +00:00
Yeicor
aef047a658 Merge remote-tracking branch 'origin/master' 2024-03-14 17:27:02 +01:00
Yeicor
d5cdd094e8 reduce idle cpu usage and add todo 2024-03-14 17:26:54 +01:00
Yeicor
9c71573934 Automatically update version to 0.6.14 2024-03-10 18:47:07 +00:00
Yeicor
8fc5ed7544 fix for export_all 2024-03-10 19:46:20 +01:00
Yeicor
1fd932dbc6 Merge remote-tracking branch 'origin/master' 2024-03-10 19:06:59 +01:00
Yeicor
539ac40e3d update readme links 2024-03-10 19:06:51 +01:00
38 changed files with 1860 additions and 1068 deletions

View File

@@ -15,7 +15,7 @@ jobs:
# will not occur. # will not occur.
- name: "Dependabot metadata" - name: "Dependabot metadata"
id: "dependabot-metadata" id: "dependabot-metadata"
uses: "dependabot/fetch-metadata@v1" uses: "dependabot/fetch-metadata@v2"
with: with:
github-token: "${{ secrets.GITHUB_TOKEN }}" github-token: "${{ secrets.GITHUB_TOKEN }}"
# Here the PR gets approved. # Here the PR gets approved.

View File

@@ -40,7 +40,7 @@ jobs:
mv "$dir/"* public/ mv "$dir/"* public/
rmdir "$dir" rmdir "$dir"
done done
- uses: "actions/configure-pages@v4" - uses: "actions/configure-pages@v5"
- uses: "actions/upload-pages-artifact@v3" - uses: "actions/upload-pages-artifact@v3"
with: with:
path: 'public' path: 'public'

View File

@@ -10,19 +10,19 @@ in a web browser.
- All [GLTF 2.0](https://www.khronos.org/gltf/) features (textures, PBR materials, animations...). - 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...). - All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
- Load multiple models at once, load external models and even images as quads. - 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. - 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. - 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). - 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 ## Usage
The [example](example) is a fully working project that shows how to use the viewer. The [example](example) is a fully working project that shows how to use the viewer.
You can play with the latest 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 (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)).
![Demo](assets/screenshot.png) ![Demo](assets/screenshot.png)

View File

@@ -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.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 Copyright (c) 2009-2011, Mozilla Foundation and contributors
All rights reserved. All rights reserved.
@@ -1290,7 +1291,7 @@ third-party archives.
The following npm package may be included in this product: 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: 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: The following npm package may be included in this product:
- napi-build-utils@1.0.2 - napi-build-utils@1.0.2
@@ -2408,7 +2439,7 @@ THE SOFTWARE.
The following npm package may be included in this product: 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: 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: The following npm packages may be included in this product:
- @gltf-transform/core@3.10.0 - @gltf-transform/core@3.10.1
- @gltf-transform/extensions@3.10.0 - @gltf-transform/extensions@3.10.1
- @gltf-transform/functions@3.10.0 - @gltf-transform/functions@3.10.1
These packages each contain the following license and notice below: 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: 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: This package contains the following license and notice below:

View File

@@ -18,6 +18,7 @@ with BuildPart() as example:
# Show it in the frontend with hot-reloading # Show it in the frontend with hot-reloading
show(example) show(example)
# %% # %%
# If running on CI, export the objects to .glb files for a static deployment # If running on CI, export the objects to .glb files for a static deployment

View File

@@ -1,13 +1,13 @@
<!--suppress SillyAssignmentJS --> <!--suppress SillyAssignmentJS -->
<script setup lang="ts"> <script lang="ts" setup>
import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef} from "vue"; import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef, watch} from "vue";
import Sidebar from "./misc/Sidebar.vue"; import Sidebar from "./misc/Sidebar.vue";
import Loading from "./misc/Loading.vue"; import Loading from "./misc/Loading.vue";
import Tools from "./tools/Tools.vue"; import Tools from "./tools/Tools.vue";
import Models from "./models/Models.vue"; import Models from "./models/Models.vue";
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs"; import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
import {settings} from "./misc/settings"; import {settings} from "./misc/settings";
import {NetworkManager, NetworkUpdateEvent} from "./misc/network"; import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
import {SceneMgr} from "./misc/scene"; import {SceneMgr} from "./misc/scene";
import {Document} from "@gltf-transform/core"; import {Document} from "@gltf-transform/core";
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
@@ -28,39 +28,67 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
const sceneDocument = shallowRef(new Document()); const sceneDocument = shallowRef(new Document());
provide('sceneDocument', {sceneDocument}); provide('sceneDocument', {sceneDocument});
const models: Ref<InstanceType<typeof Models> | null> = ref(null) const models: Ref<InstanceType<typeof Models> | null> = ref(null)
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
const disableTap = ref(false); const disableTap = ref(false);
const setDisableTap = (val: boolean) => disableTap.value = val; const setDisableTap = (val: boolean) => disableTap.value = val;
provide('disableTap', {disableTap, setDisableTap}); provide('disableTap', {disableTap, setDisableTap});
async function onModelUpdateRequest(event: NetworkUpdateEvent) { 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 // Load/unload a new batch of models to optimize rendering time
console.log("Received model update request", event.models); 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; let doc = sceneDocument.value;
for (let modelIndex in event.models) { for (let modelIndex in event.models) {
let isLast = parseInt(modelIndex) === event.models.length - 1; let isLast = parseInt(modelIndex) === event.models.length - 1;
let model = event.models[modelIndex]; let model = event.models[modelIndex];
tools.value?.removeObjectSelections(model.name);
try {
if (!model.isRemove) { if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast, isLast); doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
} else { } else {
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast); 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 sceneDocument.value = doc
triggerRef(sceneDocument); // Why not triggered automatically? triggerRef(sceneDocument); // Why not triggered automatically?
} }
async function onModelRemoveRequest(name: string) { async function onModelRemoveRequest(name: string) {
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name); await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
triggerRef(sceneDocument); // Why not triggered automatically? }));
} }
// Set up the load model event listener // Set up the load model event listener
let networkMgr = new NetworkManager(); 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)); networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
// Start loading all configured models ASAP // Start loading all configured models ASAP
for (let model of settings.preload) { for (let model of settings.preload) {
networkMgr.load(model); networkMgr.load(model);
} }
watch(viewer, (newViewer) => {
if (newViewer) {
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
' models from:</tspan>' + settings.preload.map((url) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
}
});
async function loadModelManual() { async function loadModelManual() {
const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:"); const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:");
@@ -77,24 +105,24 @@ async function loadModelManual() {
</v-main> </v-main>
<!-- The left collapsible sidebar has the list of models --> <!-- The left collapsible sidebar has the list of models -->
<sidebar :opened-init="openSidebarsByDefault" side="left" :width="300"> <sidebar :opened-init="openSidebarsByDefault" :width="300" side="left">
<template #toolbar> <template #toolbar>
<v-toolbar-title>Models</v-toolbar-title> <v-toolbar-title>Models</v-toolbar-title>
</template> </template>
<template #toolbar-items> <template #toolbar-items>
<v-btn icon="" @click="loadModelManual"> <v-btn icon="" @click="loadModelManual">
<svg-icon type="mdi" :path="mdiPlus"/> <svg-icon :path="mdiPlus" type="mdi"/>
</v-btn> </v-btn>
</template> </template>
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/> <models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
</sidebar> </sidebar>
<!-- The right collapsible sidebar has the list of tools --> <!-- The right collapsible sidebar has the list of tools -->
<sidebar :opened-init="openSidebarsByDefault" side="right" :width="48 * 3 /* buttons */ + 1 /* border? */"> <sidebar :opened-init="openSidebarsByDefault" :width="48 * 3 /* buttons */ + 1 /* border? */" side="right">
<template #toolbar> <template #toolbar>
<v-toolbar-title>Tools</v-toolbar-title> <v-toolbar-title>Tools</v-toolbar-title>
</template> </template>
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/> <tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
</sidebar> </sidebar>
</v-layout> </v-layout>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script lang="ts" setup>
import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components/index.mjs"; import {VCol, VContainer, VProgressCircular, VRow} from "vuetify/lib/components/index.mjs";
</script> </script>
<template> <template>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import {ref} from "vue"; import {ref} from "vue";
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs"; import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js' import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
@@ -16,22 +16,22 @@ const openIcon = props.side === 'left' ? mdiChevronRight : mdiChevronLeft;
</script> </script>
<template> <template>
<v-btn icon @click="opened = !opened" class="open-button" :class="side"> <v-btn :class="side" class="open-button" icon @click="opened = !opened">
<svg-icon type="mdi" :path="openIcon"/> <svg-icon :path="openIcon" type="mdi"/>
</v-btn> </v-btn>
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width"> <v-navigation-drawer v-model="opened" :location="side" :width="props.width" permanent>
<v-toolbar density="compact"> <v-toolbar density="compact">
<v-toolbar-items v-if="side == 'right'"> <v-toolbar-items v-if="side == 'right'">
<slot name="toolbar-items"></slot> <slot name="toolbar-items"></slot>
<v-btn icon @click="opened = !opened"> <v-btn icon @click="opened = !opened">
<svg-icon type="mdi" :path="mdiClose"/> <svg-icon :path="mdiClose" type="mdi"/>
</v-btn> </v-btn>
</v-toolbar-items> </v-toolbar-items>
<slot name="toolbar"></slot> <slot name="toolbar"></slot>
<v-toolbar-items v-if="side == 'left'"> <v-toolbar-items v-if="side == 'left'">
<slot name="toolbar-items"></slot> <slot name="toolbar-items"></slot>
<v-btn icon @click="opened = !opened"> <v-btn icon @click="opened = !opened">
<svg-icon type="mdi" :path="mdiClose"/> <svg-icon :path="mdiClose" type="mdi"/>
</v-btn> </v-btn>
</v-toolbar-items> </v-toolbar-items>
</v-toolbar> </v-toolbar>

View File

@@ -1,16 +1,16 @@
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three'; import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
import type {MObject3D} from "../tools/Selection.vue"; 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, center: Vector3,
vertices: Array<Vector3> vertices: Array<Vector3>
} { } {
obj.updateMatrixWorld(); let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
let pos: BufferAttribute | InterleavedBufferAttribute = obj.geometry.getAttribute('position'); let ind: BufferAttribute | null = selInfo.object.geometry.index;
let ind: BufferAttribute | null = obj.geometry.index; if (ind === null) {
if (!ind) {
ind = new BufferAttribute(new Uint16Array(pos.count), 1); ind = new BufferAttribute(new Uint16Array(pos.count), 1);
for (let i = 0; i < pos.count; i++) { for (let i = 0; i < pos.count; i++) {
ind.array[i] = i; ind.array[i] = i;
@@ -18,14 +18,14 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
} }
let center = new Vector3(); let center = new Vector3();
let vertices = []; let vertices = [];
for (let i = 0; i < ind.count; i++) { for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let index = ind.array[i]; let index = ind.getX(i)
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index)); 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); center.add(vertex);
vertices.push(vertex); vertices.push(vertex);
} }
center = center.divideScalar(ind.count); center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
return {center, vertices}; 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. * 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. * 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>, min: Array<Vector3>,
center: Array<Vector3>, center: Array<Vector3>,
max: Array<Vector3> max: Array<Vector3>

View File

@@ -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"; import {unpartition} from "@gltf-transform/functions";
let io = new WebIO(); 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). * 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 // 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 // Remove any previous model with the same name
await document.transform(dropByName(name)); await document.transform(dropByName(name));

View File

@@ -1,3 +1,5 @@
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
import {Document, type TypedArray} from '@gltf-transform/core' import {Document, type TypedArray} from '@gltf-transform/core'
import {Vector2} from "three/src/math/Vector2.js" import {Vector2} from "three/src/math/Vector2.js"
import {Vector3} from "three/src/math/Vector3.js" import {Vector3} from "three/src/math/Vector3.js"
@@ -26,7 +28,7 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
if (rawColors) { if (rawColors) {
colors = doc.createAccessor(name + 'Color') colors = doc.createAccessor(name + 'Color')
.setArray(new Float32Array(rawColors) as TypedArray) .setArray(new Float32Array(rawColors) as TypedArray)
.setType('VEC3') .setType('VEC4')
.setBuffer(buffer); .setBuffer(buffer);
} }
const material = doc.createMaterial(name + 'Material') const material = doc.createMaterial(name + 'Material')
@@ -39,6 +41,11 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
if (rawColors) { if (rawColors) {
geometry.setAttribute('COLOR_0', colors) 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 mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any) const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
scene.addChild(node) 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. * Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
*/ */
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) { export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
let rawIndices = [0, 1, 2, 3, 4, 5];
let rawPositions = [ let rawPositions = [
[0, 0, 0, size.x, 0, 0], 0, 0, 0, size.x, 0, 0,
[0, 0, 0, 0, size.y, 0], 0, 0, 0, 0, size.y, 0,
[0, 0, 0, 0, 0, -size.z], 0, 0, 0, 0, 0, -size.z,
]; ];
let rawIndices = [0, 1];
let rawColors = [ let rawColors = [
[...(AxesColors.x[0]), ...(AxesColors.x[1])], ...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
[...(AxesColors.y[0]), ...(AxesColors.y[1])], ...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
[...(AxesColors.z[0]), ...(AxesColors.z[1])], ...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
].map(g => g.map(x => x / 255.0)); ].map(x => x / 255.0);
buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes'); // Axes at (0, 0, 0)
buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes'); buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes'); buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
buildSimpleGltf(doc, [0, 0, 0], [0], null, transform, '__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. * 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. * 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 // 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 axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
for (let positive of [1, -1]) { for (let positive of [1, -1]) {
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive)); 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.x) size2.set(size.z, size.y);
if (axis.y) size2.set(size.x, size.z); if (axis.y) size2.set(size.x, size.z);
if (axis.z) size2.set(size.x, size.y); if (axis.z) size2.set(size.x, size.y);
let transform = baseTransform.clone().multiply(translation).multiply(rotation); let transform = new Matrix4().multiply(translation).multiply(rotation);
newGridPlane(doc, size2, transform, divisions); 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 rawPositions = [];
const rawIndices = []; const rawIndices = [];
// Build the grid as triangles // 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 + 1, baseIndex2 + 2);
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3); 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
View 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;
}

View File

@@ -2,27 +2,29 @@ import {settings} from "./settings";
const batchTimeout = 250; // ms const batchTimeout = 250; // ms
class NetworkUpdateEventModel { export class NetworkUpdateEventModel {
name: string; name: string;
url: string; url: string;
// TODO: Detect and manage instances of the same object (same hash, different name) // TODO: Detect and manage instances of the same object (same hash, different name)
hash: string | null; 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.name = name;
this.url = url; this.url = url;
this.hash = hash; this.hash = hash;
this.isRemove = isDelete; this.isRemove = isRemove;
} }
} }
export class NetworkUpdateEvent extends Event { export class NetworkUpdateEvent extends Event {
models: NetworkUpdateEventModel[]; models: NetworkUpdateEventModel[];
disconnect: () => void;
constructor(models: NetworkUpdateEventModel[]) { constructor(models: NetworkUpdateEventModel[], disconnect: () => void) {
super("update"); super("update");
this.models = models; this.models = models;
this.disconnect = disconnect;
} }
} }
@@ -57,49 +59,82 @@ export class NetworkManager extends EventTarget {
} }
} }
private async monitorDevServer(url: URL) { private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
while (!stop()) {
try { try {
// WARNING: This will spam the console logs with failed requests when the server is down // WARNING: This will spam the console logs with failed requests when the server is down
let response = await fetch(url.toString()); const controller = new AbortController();
let response = await fetch(url.toString(), {signal: controller.signal});
// console.log("Monitoring", url.toString(), response); // console.log("Monitoring", url.toString(), response);
if (response.status === 200) { if (response.status === 200) {
let lines = readLinesStreamings(response.body!.getReader()); let lines = readLinesStreamings(response.body!.getReader());
for await (let line of lines) { for await (let line of lines) {
if (stop()) break;
if (!line || !line.startsWith("data:")) continue; if (!line || !line.startsWith("data:")) continue;
let data = JSON.parse(line.slice(5)); let data: { name: string, hash: string, is_remove: boolean | null } = JSON.parse(line.slice(5));
// console.debug("WebSocket message", data); // console.debug("WebSocket message", data);
let urlObj = new URL(url); let urlObj = new URL(url);
urlObj.searchParams.delete("api_updates"); urlObj.searchParams.delete("api_updates");
urlObj.searchParams.set("api_object", data.name); urlObj.searchParams.set("api_object", data.name);
this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove); 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)
} }
setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs); await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
return; }
} }
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean) { private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
let prevHash = this.knownObjectHashes[name]; }) {
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash); // console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
if (!hash || hash !== prevHash || isRemove) {
if (!isRemove) { // We only care about the latest update per model name
this.knownObjectHashes[name] = hash; this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
} else {
delete this.knownObjectHashes[name]; // Add the new model to the list of updates and dispatch the early update
} let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove); this.bufferedUpdates.push(upd);
this.bufferedUpdates.push(newModel); this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
// Optimization: try to batch updates automatically for faster rendering // Optimization: try to batch updates automatically for faster rendering
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout); if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
this.batchTimeout = setTimeout(() => { this.batchTimeout = setTimeout(() => {
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates)); // 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 = []; this.bufferedUpdates = [];
}, batchTimeout); }, batchTimeout);
} }
}
} }
async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) { async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) {

View File

@@ -11,12 +11,15 @@ export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */ /** 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> { 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 loadStart = performance.now();
let loadNetworkEnd: number;
try {
// Start merging into the current document, replacing or adding as needed // Start merging into the current document, replacing or adding as needed
document = await mergePartial(url, name, document); 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) { if (updateHelpers) {
// Reload the helpers to fit the new model // Reload the helpers to fit the new model
await this.reloadHelpers(sceneUrl, document, reloadScene); await this.reloadHelpers(sceneUrl, document, reloadScene);
@@ -29,21 +32,9 @@ export class SceneMgr {
document = await this.showCurrentDoc(sceneUrl, document); document = await this.showCurrentDoc(sceneUrl, document);
console.log("Scene displayed in", performance.now() - displayStart, "ms"); console.log("Scene displayed in", performance.now() - displayStart, "ms");
} }
return document;
} }
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> { return document;
let bb = SceneMgr.getBoundingBox(document);
if (!bb) return document;
// Create the helper axes and grid box
let helpersDoc = new Document();
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
} }
static getBoundingBox(document: Document): Box3 | null { static getBoundingBox(document: Document): Box3 | null {
@@ -87,6 +78,19 @@ export class SceneMgr {
return document; return document;
} }
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> {
let bb = SceneMgr.getBoundingBox(document);
if (!bb) return document;
// Create the helper axes and grid box
let helpersDoc = new Document();
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
}
/** Serializes the current document into a GLB and updates the viewerSrc */ /** Serializes the current document into a GLB and updates the viewerSrc */
private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> { private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
// Make sure the document is fully loaded and ready to be shown // Make sure the document is fully loaded and ready to be shown
@@ -95,7 +99,7 @@ export class SceneMgr {
// Serialize the document into a GLB and update the viewerSrc // Serialize the document into a GLB and update the viewerSrc
let buffer = await toBuffer(document); let buffer = await toBuffer(document);
let blob = new Blob([buffer], {type: 'model/gltf-binary'}); 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); sceneUrl.value = URL.createObjectURL(blob);
return document; return document;

View File

@@ -12,12 +12,17 @@ export const settings = {
// Websocket URLs automatically listen for new models from the python backend // Websocket URLs automatically listen for new models from the python backend
"dev+http://127.0.0.1:32323/" "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 */ displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100, monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000, monitorOpenTimeoutMs: 1000,
// ModelViewer settings // ModelViewer settings
autoplay: true, autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look', arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1, exposure: 1,
shadowIntensity: 0, shadowIntensity: 0,
background: '', background: '',

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import { import {
VBtn, VBtn,
VBtnToggle, VBtnToggle,
@@ -12,8 +12,8 @@ import {
VTooltip, VTooltip,
} from "vuetify/lib/components/index.mjs"; } from "vuetify/lib/components/index.mjs";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf"; import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
import {Document, Mesh} from "@gltf-transform/core"; import {Mesh} from "@gltf-transform/core";
import {inject, ref, type ShallowRef, watch} from "vue"; import {ref, watch} from "vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import { import {
mdiCircleOpacity, mdiCircleOpacity,
@@ -22,16 +22,18 @@ import {
mdiRectangle, mdiRectangle,
mdiRectangleOutline, mdiRectangleOutline,
mdiSwapHorizontal, mdiSwapHorizontal,
mdiVectorLine,
mdiVectorRectangle mdiVectorRectangle
} from '@mdi/js' } from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon'; import SvgIcon from '@jamescoyle/vue-icon';
import {SceneMgr} from "../misc/scene";
import {BackSide, FrontSide} from "three/src/constants.js"; import {BackSide, FrontSide} from "three/src/constants.js";
import {Box3} from "three/src/math/Box3.js"; import {Box3} from "three/src/math/Box3.js";
import {Color} from "three/src/math/Color.js"; import {Color} from "three/src/math/Color.js";
import {Plane} from "three/src/math/Plane.js"; import {Plane} from "three/src/math/Plane.js";
import {Vector3} from "three/src/math/Vector3.js"; import {Vector3} from "three/src/math/Vector3.js";
import type {MObject3D} from "../tools/Selection.vue"; import type {MObject3D} from "../tools/Selection.vue";
import {toLineSegments} from "../misc/lines.js";
import {settings} from "../misc/settings.js"
const props = defineProps<{ const props = defineProps<{
meshes: Array<Mesh>, 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" let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
// Reactive properties // Count the number of faces, edges and vertices
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]}); let faceCount = ref(-1);
const opacity = defineModel<number>("opacity", {default: 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 clipPlaneX = ref(1);
const clipPlaneSwappedX = ref(false); const clipPlaneSwappedX = ref(false);
const clipPlaneY = ref(1); const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false); const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1); const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false); const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(settings.edgeWidth);
// Count the number of faces, edges and vertices // Misc properties
let faceCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length).reduce((a, b) => a + b, 0) const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
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) const opacity = defineModel<number>("opacity", {default: 1});
let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0) const wireframe = ref(false);
// 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)
// Listeners for changes in the properties (or viewer reloads) // Listeners for changes in the properties (or viewer reloads)
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) { function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
@@ -67,13 +68,10 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model; let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return; 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) => { sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) { if (child.userData[extrasNameKey] === modelName) {
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh' 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' let childIsVertex = child.type == 'Points'
if (childIsFace || childIsEdge || childIsVertex) { if (childIsFace || childIsEdge || childIsVertex) {
let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1); let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1);
@@ -87,16 +85,12 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
scene.queueRender() scene.queueRender()
} }
watch(enabledFeatures, onEnabledFeaturesChange); watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
function onOpacityChange(newOpacity: number) { function onOpacityChange(newOpacity: number) {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model; let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return; 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) => { sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) { if (child.userData[extrasNameKey] === modelName) {
if (child.material && child.material.opacity !== newOpacity) { if (child.material && child.material.opacity !== newOpacity) {
@@ -111,7 +105,22 @@ function onOpacityChange(newOpacity: number) {
watch(opacity, onOpacityChange); 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() { function onClipPlanesChange() {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
@@ -125,22 +134,25 @@ function onClipPlanesChange() {
if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) { if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) {
// Global value for all models, once set it cannot be unset (unknown for other models...) // Global value for all models, once set it cannot be unset (unknown for other models...)
props.viewer.renderer.threeRenderer.localClippingEnabled = true; props.viewer.renderer.threeRenderer.localClippingEnabled = true;
// Due to model-viewer's camera manipulation, the bounding box needs to be transformed // Get the bounding box containing all features of this model
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value); bbox = new Box3();
if (!boundingBox) return; // No models. Should not happen. sceneModel.traverse((child: MObject3D) => {
bbox = boundingBox.translate(scene.getTarget()); if (child.userData[extrasNameKey] === modelName) {
bbox.expandByObject(child);
}
});
} }
sceneModel.traverse((child: MObject3D) => { sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) { if (child.userData[extrasNameKey] === modelName) {
if (child.material) { if (child.material) {
if (bbox) { if (bbox?.isEmpty() == false) {
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x); 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 offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y);
let offsetZ = bbox.min.y + clipPlaneZ.value * (bbox.max.y - bbox.min.y); let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z);
let planes = [ let planes = [
new Plane(new Vector3(-1, 0, 0), offsetX), new Plane(new Vector3(-1, 0, 0), offsetX),
new Plane(new Vector3(0, 0, 1), offsetY), new Plane(new Vector3(0, -1, 0), offsetY),
new Plane(new Vector3(0, -1, 0), offsetZ), new Plane(new Vector3(0, 0, 1), -offsetZ),
]; ];
if (clipPlaneSwappedX.value) planes[0].negate(); if (clipPlaneSwappedX.value) planes[0].negate();
if (clipPlaneSwappedY.value) planes[1].negate(); if (clipPlaneSwappedY.value) planes[1].negate();
@@ -166,20 +178,106 @@ watch(clipPlaneZ, onClipPlanesChange);
watch(clipPlaneSwappedX, onClipPlanesChange); watch(clipPlaneSwappedX, onClipPlanesChange);
watch(clipPlaneSwappedY, onClipPlanesChange); watch(clipPlaneSwappedY, onClipPlanesChange);
watch(clipPlaneSwappedZ, onClipPlanesChange); 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() { function onModelLoad() {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model; let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return; 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 // Count the number of faces, edges and vertices
// of not actually removing the primitives from the scene graph 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> = []; let childrenToAdd: Array<MObject3D> = [];
sceneModel.traverse((child: MObject3D) => { sceneModel.traverse((child: MObject3D) => {
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
if (child.userData[extrasNameKey] === modelName) { if (child.userData[extrasNameKey] === modelName) {
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') { 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: // We could implement cutting planes using the stencil buffer:
// https://threejs.org/examples/?q=clipping#webgl_clipping_stencil // https://threejs.org/examples/?q=clipping#webgl_clipping_stencil
// But this is buggy for lots of models, so instead we just draw // But this is buggy for lots of models, so instead we just draw
@@ -193,112 +291,126 @@ function onModelLoad() {
backChild.material = child.material.clone(); backChild.material = child.material.clone();
backChild.material.side = BackSide; backChild.material.side = BackSide;
backChild.material.color = new Color(0.25, 0.25, 0.25) backChild.material.color = new Color(0.25, 0.25, 0.25)
backChild.userData.noHit = true;
child.userData.backChild = backChild; child.userData.backChild = backChild;
childrenToAdd.push(backChild as MObject3D); 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)); childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child));
scene.queueRender()
// Furthermore... // Furthermore...
// Enabled features may have been reset after a reload // Enabled features may have been reset after a reload
onEnabledFeaturesChange(enabledFeatures.value) onEnabledFeaturesChange(enabledFeatures.value)
// Opacity may have been reset after a reload // Opacity may have been reset after a reload
onOpacityChange(opacity.value) onOpacityChange(opacity.value)
// Wireframe may have been reset after a reload
onWireframeChange(wireframe.value)
// Clip planes may have been reset after a reload // Clip planes may have been reset after a reload
onClipPlanesChange() 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 // props.viewer.elem may not yet be available, so we need to wait for it
props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad)) const onViewerReady = (viewer: InstanceType<typeof ModelViewerWrapper>) => {
viewer?.onElemReady((elem: HTMLElement) => {
elem.addEventListener('before-render', onModelLoad);
elem.addEventListener('camera-change', onClipPlanesChange);
});
};
if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) as any, onViewerReady);
</script> </script>
<template> <template>
<v-expansion-panel :value="modelName"> <v-expansion-panel :value="modelName">
<v-expansion-panel-title expand-icon="hide-this-icon" collapse-icon="hide-this-icon"> <v-expansion-panel-title collapse-icon="hide-this-icon" expand-icon="hide-this-icon">
<v-btn-toggle v-model="enabledFeatures" multiple @click.stop color="surface-light"> <v-btn-toggle v-model="enabledFeatures" color="surface-light" multiple @click.stop>
<v-btn icon> <v-btn icon>
<v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip> <v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip>
<svg-icon type="mdi" :path="mdiRectangle" :rotate="90"></svg-icon> <svg-icon :path="mdiRectangle" :rotate="90" type="mdi"></svg-icon>
</v-btn> </v-btn>
<v-btn icon> <v-btn icon>
<v-tooltip activator="parent">Toggle Edges ({{ edgeCount }})</v-tooltip> <v-tooltip activator="parent">Toggle Edges ({{ edgeCount }})</v-tooltip>
<svg-icon type="mdi" :path="mdiRectangleOutline" :rotate="90"></svg-icon> <svg-icon :path="mdiRectangleOutline" :rotate="90" type="mdi"></svg-icon>
</v-btn> </v-btn>
<v-btn icon> <v-btn icon>
<v-tooltip activator="parent">Toggle Vertices ({{ vertexCount }})</v-tooltip> <v-tooltip activator="parent">Toggle Vertices ({{ vertexCount }})</v-tooltip>
<svg-icon type="mdi" :path="mdiVectorRectangle" :rotate="90"></svg-icon> <svg-icon :path="mdiVectorRectangle" :rotate="90" type="mdi"></svg-icon>
</v-btn> </v-btn>
</v-btn-toggle> </v-btn-toggle>
<div class="model-name">{{ modelName }}</div> <div class="model-name">{{ modelName }}</div>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click.stop="emit('remove')"> <v-btn icon @click.stop="emit('remove')">
<v-tooltip activator="parent">Remove</v-tooltip> <v-tooltip activator="parent">Remove</v-tooltip>
<svg-icon type="mdi" :path="mdiDelete"></svg-icon> <svg-icon :path="mdiDelete" type="mdi"></svg-icon>
</v-btn> </v-btn>
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
<v-slider v-model="opacity" hide-details min="0" max="1" :step="0.1"> <v-slider v-model="opacity" :step="0.1" hide-details max="1" min="0">
<template v-slot:prepend> <template v-slot:prepend>
<v-tooltip activator="parent">Change opacity</v-tooltip> <v-tooltip activator="parent">Change opacity</v-tooltip>
<svg-icon type="mdi" :path="mdiCircleOpacity"></svg-icon> <svg-icon :path="mdiCircleOpacity" type="mdi"></svg-icon>
</template>
<template v-slot:append>
<v-tooltip activator="parent">Wireframe</v-tooltip>
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
</template>
</v-slider>
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
<template v-slot:prepend>
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
<svg-icon :path="mdiVectorLine" type="mdi"></svg-icon>
</template> </template>
</v-slider> </v-slider>
<v-divider></v-divider> <v-divider></v-divider>
<v-slider v-model="clipPlaneX" hide-details min="0" max="1"> <v-slider v-model="clipPlaneX" hide-details max="1" min="0">
<template v-slot:prepend> <template v-slot:prepend>
<v-tooltip activator="parent">Clip plane X</v-tooltip> <v-tooltip activator="parent">Clip plane X</v-tooltip>
<svg-icon type="mdi" :path="mdiCube" :rotate="120"></svg-icon> <svg-icon :path="mdiCube" :rotate="120" type="mdi"></svg-icon>
X X
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-tooltip activator="parent">Swap clip plane X</v-tooltip> <v-tooltip activator="parent">Swap clip plane X</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline" <v-checkbox-btn v-model="clipPlaneSwappedX" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedX"> trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label> <template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon> <svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
</v-slider> </v-slider>
<v-slider v-model="clipPlaneY" hide-details min="0" max="1"> <v-slider v-model="clipPlaneZ" hide-details max="1" min="0">
<template v-slot:prepend> <template v-slot:prepend>
<v-tooltip activator="parent">Clip plane Y</v-tooltip> <v-tooltip activator="parent">Clip plane Y</v-tooltip>
<svg-icon type="mdi" :path="mdiCube" :rotate="-120"></svg-icon> <svg-icon :path="mdiCube" :rotate="-120" type="mdi"></svg-icon>
Y Y
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip> <v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline" <v-checkbox-btn v-model="clipPlaneSwappedZ" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedY"> trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label> <template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon> <svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
</v-slider> </v-slider>
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1"> <v-slider v-model="clipPlaneY" hide-details max="1" min="0">
<template v-slot:prepend> <template v-slot:prepend>
<v-tooltip activator="parent">Clip plane Z</v-tooltip> <v-tooltip activator="parent">Clip plane Z</v-tooltip>
<svg-icon type="mdi" :path="mdiCube"></svg-icon> <svg-icon :path="mdiCube" type="mdi"></svg-icon>
Z Z
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip> <v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline" <v-checkbox-btn v-model="clipPlaneSwappedY" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedZ"> trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label> <template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon> <svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
@@ -359,4 +471,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
.mdi-checkbox-marked-outline { /* HACK: mdi is not fully imported, only required icons... */ .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>'); 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> </style>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import {VExpansionPanels} from "vuetify/lib/components/index.mjs"; import {VExpansionPanels} from "vuetify/lib/components/index.mjs";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {Document, Mesh} from "@gltf-transform/core"; import {Document, Mesh} from "@gltf-transform/core";
@@ -36,9 +36,7 @@ function onRemove(mesh: Mesh) {
} }
function findModel(name: string) { function findModel(name: string) {
console.log('Find model', name);
if (!expandedNames.value.includes(name)) expandedNames.value.push(name); if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
console.log('Expanded', expandedNames.value);
} }
defineExpose({findModel}) defineExpose({findModel})

2
frontend/shims.d.ts vendored
View File

@@ -1,3 +1,5 @@
// Avoids typescript error when importing some files // Avoids typescript error when importing some files
declare module '@jamescoyle/vue-icon' declare module '@jamescoyle/vue-icon'
declare module 'three-orientation-gizmo/src/OrientationGizmo' 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'

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
// License text for all dependencies, only downloaded when/if needed // License text for all dependencies, only downloaded when/if needed
// @ts-ignore // @ts-ignore
import licenseText from "../../assets/licenses.txt?raw"; import licenseText from "../../assets/licenses.txt?raw";

View File

@@ -1,18 +1,16 @@
<script setup lang="ts"> <script lang="ts" setup>
import {onMounted, onUpdated, ref} from "vue"; import {onMounted, onUpdated, ref} from "vue";
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo"; import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
import type {ModelViewerElement} from '@google/model-viewer';
// Optimized minimal dependencies from three // Optimized minimal dependencies from three
import {Vector3} from "three/src/math/Vector3.js"; import {Vector3} from "three/src/math/Vector3.js";
import {Matrix4} from "three/src/math/Matrix4.js"; import {Matrix4} from "three/src/math/Matrix4.js";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
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; const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> }>();
const props = defineProps<{ elem: ModelViewerElement | null, scene: ModelScene }>();
function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement { function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement {
// noinspection SpellCheckingInspection // noinspection SpellCheckingInspection
@@ -33,21 +31,26 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen
} }
// Append and listen for events // Append and listen for events
gizmo.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => { gizmo.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => {
let lookFrom = scene.getCamera().position.clone(); if (!props.viewer.elem || !props.viewer.controls) return;
let lookAt = scene.getTarget().clone().add(scene.target.position); // Animate the controls to the new wanted angle
let magnitude = lookFrom.clone().sub(lookAt).length() const controls = props.viewer.controls;
let direction = new Vector3(axis.direction.x, axis.direction.y, axis.direction.z); const {theta: curTheta/*, phi: curPhi*/} = (controls as any).goalSpherical;
let newLookFrom = lookAt.clone().add(direction.clone().multiplyScalar(magnitude)); let wantedTheta = NaN;
//console.log("New camera position", newLookFrom) let wantedPhi = NaN;
scene.getCamera().position.copy(newLookFrom); let attempt = 0
scene.getCamera().lookAt(lookAt); while ((attempt == 0 || curTheta == wantedTheta) && attempt < 2) {
if ((scene as any).__perspectiveCamera) { // HACK: Make the hacky ortho also work if (attempt > 0) { // Flip the camera if the user clicks on the same axis
(scene as any).__perspectiveCamera.position.copy(newLookFrom); axis.direction.x = -axis.direction.x;
(scene as any).__perspectiveCamera.lookAt(lookAt); axis.direction.y = -axis.direction.y;
axis.direction.z = -axis.direction.z;
} }
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z);
wantedPhi = Math.asin(-axis.direction.y) + Math.PI / 2;
attempt++;
}
controls.setOrbit(wantedTheta, wantedPhi);
props.viewer.elem?.dispatchEvent(new CustomEvent('camera-change', {detail: {source: 'none'}}))
scene.queueRender(); scene.queueRender();
requestIdleCallback(() => props.elem?.dispatchEvent(
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100})
} }
return gizmo; return gizmo;
} }
@@ -65,9 +68,9 @@ function updateGizmo() {
} }
let reinstall = () => { let reinstall = () => {
if(!container.value) return; if (!container.value) return;
if (gizmo) container.value.removeChild(gizmo); if (gizmo) container.value.removeChild(gizmo);
gizmo = createGizmo(container.value, props.scene as ModelScene) as typeof gizmo; gizmo = createGizmo(container.value, props.viewer.scene!! as any) as typeof gizmo;
container.value.appendChild(gizmo); container.value.appendChild(gizmo);
requestIdleCallback(updateGizmo, {timeout: 250}); // Low priority updates requestIdleCallback(updateGizmo, {timeout: 250}); // Low priority updates
} }

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import {defineModel, inject, ref, type ShallowRef, watch} from "vue"; import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs"; import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
import SvgIcon from '@jamescoyle/vue-icon'; import SvgIcon from '@jamescoyle/vue-icon';
@@ -6,44 +6,50 @@ import type {ModelViewerElement} from '@google/model-viewer';
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js'; import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
import type {Intersection, Material, Mesh, Object3D} from "three"; 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 type ModelViewerWrapperT from "../viewer/ModelViewerWrapper.vue";
import {extrasNameKey} from "../misc/gltf"; import {extrasNameKey} from "../misc/gltf";
import {SceneMgr} from "../misc/scene"; import {SceneMgr} from "../misc/scene";
import {Document} from "@gltf-transform/core"; import {Document} from "@gltf-transform/core";
import {AxesColors} from "../misc/helpers"; import {AxesColors} from "../misc/helpers";
import {distances} from "../misc/distances"; import {distances} from "../misc/distances";
import {highlight, highlightUndo, hitToSelectionInfo, type SelectionInfo} from "./selection";
export type MObject3D = Mesh & { export type MObject3D = Mesh & {
userData: { noHit?: boolean }, 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 props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
let emit = defineEmits<{ findModel: [string] }>(); let emit = defineEmits<{ findModel: [string] }>();
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!; let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
let selectionEnabled = ref(false); 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 highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
let showDistances = ref<Boolean>(true); let showDistances = ref<Boolean>(true);
let mouseDownAt: [number, number] | null = null; let mouseDownAt: [number, number] | null = null;
let mouseDownTime = 0;
let selectFilter = ref('Any (S)'); let selectFilter = ref('Any (S)');
const raycaster = new Raycaster(); const raycaster = new Raycaster();
let selectionMoveListener = (event: MouseEvent) => { let mouseDownListener = (event: MouseEvent) => {
mouseDownAt = [event.clientX, event.clientY]; mouseDownAt = [event.clientX, event.clientY];
mouseDownTime = performance.now();
if (!selectionEnabled.value) return; if (!selectionEnabled.value) return;
}; };
let selectionListener = (event: MouseEvent) => { let mouseUpListener = (event: MouseEvent) => {
// If the mouse moved while clicked (dragging), avoid selection logic // If the mouse moved while clicked (dragging), avoid selection logic
if (mouseDownAt) { if (mouseDownAt) {
let [x, y] = mouseDownAt; let [x, y] = mouseDownAt;
mouseDownAt = null; 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; return;
} }
} }
@@ -92,8 +98,27 @@ let selectionListener = (event: MouseEvent) => {
// let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray") // let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
// setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000) // setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
// Find all hit objects and select the wanted one based on the filter // Find all hit objects and raycast the wanted ones based on the filter
const hits = raycaster.intersectObject(scene, true); 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 let hit = hits
// Check feasibility // Check feasibility
.filter((hit: Intersection<Object3D>) => { .filter((hit: Intersection<Object3D>) => {
@@ -106,7 +131,7 @@ let selectionListener = (event: MouseEvent) => {
(isFace && selectFilter.value === '(F)aces') || (isFace && selectFilter.value === '(F)aces') ||
(isEdge && selectFilter.value === '(E)dges') || (isEdge && selectFilter.value === '(E)dges') ||
(isVertex && selectFilter.value === '(V)ertices'); (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 for highlighting partially hidden edges/vertices
.sort((a, b) => { .sort((a, b) => {
@@ -123,17 +148,19 @@ let selectionListener = (event: MouseEvent) => {
}) })
// Return the best hit // Return the best hit
[0] as Intersection<MObject3D> | undefined; [0] as Intersection<MObject3D> | undefined;
// console.log('Hit', hit)
if (!highlightNextSelection.value[0]) { if (!highlightNextSelection.value[0]) {
// If we are selecting, toggle the selection or deselect all if no hit // 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 // 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) { if (wasSelected) {
deselect(hit) deselect(selInfo)
} else { } else {
select(hit) select(selInfo)
} }
} else { } else {
deselectAll(); deselectAll();
@@ -149,34 +176,22 @@ let selectionListener = (event: MouseEvent) => {
scene.queueRender() // Force rerender of model-viewer scene.queueRender() // Force rerender of model-viewer
} }
function select(hit: Intersection<MObject3D>) { function select(selInfo: SelectionInfo) {
// console.log('Selecting', hit.object.name) // console.log('Selecting', selInfo.object.name)
if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) { if (selected.value.find((m) => m.getKey() === selInfo.getKey()) === undefined) {
selected.value.push(hit); selected.value.push(selInfo);
} }
hit.object.material.__prevBaseColorFactor = [ highlight(selInfo);
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;
} }
function deselect(hit: Intersection<MObject3D>, alsoRemove = true) { function deselect(selInfo: SelectionInfo, alsoRemove = true) {
// console.log('Deselecting', hit.object.name) // console.log('Deselecting', selInfo.object.name)
if (alsoRemove) { if (alsoRemove) {
// Remove the matching object from the selection // 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); selected.value.splice(toRemove, 1);
} }
if (hit.object.material.__prevBaseColorFactor) { highlightUndo(selInfo);
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;
}
} }
function deselectAll(alsoRemove = true) { function deselectAll(alsoRemove = true) {
@@ -241,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
viewer.onElemReady((elem: ModelViewerElement) => { viewer.onElemReady((elem: ModelViewerElement) => {
if (hasListeners) return; if (hasListeners) return;
hasListeners = true; hasListeners = true;
elem.addEventListener('mouseup', selectionListener); elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging elem.addEventListener('mouseup', mouseUpListener);
elem.addEventListener('load', () => { elem.addEventListener('before-render', () => {
// 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) { if (firstLoad) {
toggleShowBoundingBox(); toggleShowBoundingBox();
firstLoad = false; firstLoad = false;
} else {
updateBoundingBox();
} }
}); });
elem.addEventListener('camera-change', onCameraChange); elem.addEventListener('camera-change', onCameraChange);
@@ -273,9 +303,8 @@ function updateBoundingBox() {
if (selected.value.length > 0) { if (selected.value.length > 0) {
bb = new Box3(); bb = new Box3();
for (let hit of selected.value) { for (let hit of selected.value) {
bb.expandByObject(hit.object); bb.union(hit.getBox())
} }
bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget()));
} else { } else {
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value); let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
if (!boundingBox) return; // No models. Should not happen. if (!boundingBox) return; // No models. Should not happen.
@@ -380,9 +409,7 @@ function updateDistances() {
} }
// Add lines (if not already added) // Add lines (if not already added)
let objA = selected.value[0].object; let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
let objB = selected.value[1].object;
let {min, center, max} = distances(objA, objB, props.viewer?.scene);
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange"); 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(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"); ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
@@ -396,6 +423,8 @@ function updateDistances() {
return; return;
} }
defineExpose({deselect, updateBoundingBox, updateDistances});
// Add keyboard shortcuts // Add keyboard shortcuts
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if (event.key === 's') { if (event.key === 's') {
@@ -433,52 +462,41 @@ window.addEventListener('keydown', (event) => {
</script> </script>
<template> <template>
<div class="select-parent"> <v-btn :color="selectionEnabled ? 'surface-light' : ''" icon @click="toggleSelection">
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }} <v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
</v-tooltip> </v-tooltip>
<svg-icon type="mdi" :path="mdiCursorDefaultClick"/> <svg-icon :path="mdiCursorDefaultClick" type="mdi"/>
</v-btn> </v-btn>
<v-tooltip :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()" :open-on-click="false"> <v-tooltip :open-on-click="false" :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-select v-bind="props" class="select-only" variant="underlined" <v-select v-model="selectFilter" :items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']" class="select-only"
:items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']" v-bind="props"
v-model="selectFilter"/> variant="underlined"/>
</template> </template>
</v-tooltip> </v-tooltip>
</div> <v-btn :color="highlightNextSelection[0] ? 'surface-light' : ''" icon @click="toggleHighlightNextSelection">
<v-btn icon @click="toggleHighlightNextSelection" :color="highlightNextSelection[0] ? 'surface-light' : ''">
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip> <v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
<svg-icon type="mdi" :path="mdiFeatureSearch"/> <svg-icon :path="mdiFeatureSearch" type="mdi"/>
</v-btn> </v-btn>
<v-btn icon @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''"> <v-btn :color="showBoundingBox ? 'surface-light' : ''" icon @click="toggleShowBoundingBox">
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection (b)ounds' : 'Show selection (b)ounds' }} <v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection (b)ounds' : 'Show selection (b)ounds' }}
</v-tooltip> </v-tooltip>
<svg-icon type="mdi" :path="mdiCubeOutline"/> <svg-icon :path="mdiCubeOutline" type="mdi"/>
</v-btn> </v-btn>
<v-btn icon @click="toggleShowDistances" :color="showDistances ? 'surface-light' : ''"> <v-btn :color="showDistances ? 'surface-light' : ''" icon @click="toggleShowDistances">
<v-tooltip activator="parent"> <v-tooltip activator="parent">
{{ showDistances ? 'Hide selection (d)istances' : 'Show (d)istances (when a pair of features is selected)' }} {{ showDistances ? 'Hide selection (d)istances' : 'Show (d)istances (when a pair of features is selected)' }}
</v-tooltip> </v-tooltip>
<svg-icon type="mdi" :path="mdiRuler"/> <svg-icon :path="mdiRuler" type="mdi"/>
</v-btn> </v-btn>
</template> </template>
<style scoped> <style scoped>
/* Very hacky styling... */
.select-parent {
height: 48px;
}
.select-parent .v-btn {
position: relative;
top: -20px;
}
.select-only { .select-only {
display: inline-block; float: right;
width: calc(100% - 48px); height: 36px;
position: relative; position: relative;
top: -12px; top: -12px;
width: calc(100% - 48px);
} }
</style> </style>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import { import {
VBtn, VBtn,
VCard, VCard,
@@ -16,11 +16,10 @@ import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js' import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon'; import SvgIcon from '@jamescoyle/vue-icon';
import type {ModelViewerElement} from '@google/model-viewer'; 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 Loading from "../misc/Loading.vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.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({ const SelectionComponent = defineAsyncComponent({
loader: () => import("./Selection.vue"), loader: () => import("./Selection.vue"),
@@ -39,10 +38,10 @@ const LicensesDialogContent = defineAsyncComponent({
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>(); let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
const emit = defineEmits<{ findModel: [string] }>() const emit = defineEmits<{ findModel: [string] }>()
let selection: Ref<Array<Intersection<MObject3D>>> = ref([]); let selection: Ref<Array<SelectionInfo>> = ref([]);
let selectionFaceCount = () => selection.value.filter((s) => s.object.type == "Mesh" || s.object.type == "SkinnedMesh").length let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
let selectionEdgeCount = () => selection.value.filter((s) => s.object.type == "Line").length let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
let selectionVertexCount = () => selection.value.filter((s) => s.object.type == "Points").length let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
function syncOrthoCamera(force: boolean) { function syncOrthoCamera(force: boolean) {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
@@ -57,14 +56,14 @@ function syncOrthoCamera(force: boolean) {
let h = perspectiveWidthAtCenter / scene.aspect; let h = perspectiveWidthAtCenter / scene.aspect;
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far); (scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
scene.camera.position.copy(perspectiveCam.position); scene.camera.position.copy(perspectiveCam.position);
scene.camera.lookAt(lookAtCenter); scene.camera.rotation.copy(perspectiveCam.rotation);
if (force) scene.queueRender() // Force rerender of model-viewer if (force) scene.queueRender() // Force rerender of model-viewer
requestAnimationFrame(() => syncOrthoCamera(false)); requestAnimationFrame(() => syncOrthoCamera(false));
} }
} }
let toggleProjectionText = ref('PERSP'); // Default to perspective camera let toggleProjectionText = ref('PERSP'); // Default to perspective camera
function toggleProjection() { async function toggleProjection() {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
if (!scene) return; if (!scene) return;
let prevCam = scene.camera; let prevCam = scene.camera;
@@ -79,16 +78,16 @@ function toggleProjection() {
scene.queueRender() // Force rerender of model-viewer scene.queueRender() // Force rerender of model-viewer
} }
toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP'; toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP';
// The camera change may take a few frames to take effect, dispatch the event after a delay // The camera change may take a frame to take effect, dispatch the event after a delay
requestIdleCallback(() => props.viewer?.elem?.dispatchEvent( await new Promise((resolve) => requestAnimationFrame(resolve));
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100}) props.viewer?.elem?.dispatchEvent(new CustomEvent('camera-change', {detail: {source: 'none'}}));
} }
async function centerCamera() { async function centerCamera() {
let viewerEl: ModelViewerElement | null | undefined = props.viewer?.elem; let viewerEl: ModelViewerElement | null | undefined = props.viewer?.elem;
if (!viewerEl) return; if (!viewerEl) return;
await viewerEl.updateFraming(); props.viewer?.scene?.setTarget(0, 0, 0); // Center the target
viewerEl.zoom(3); viewerEl.zoom(-1000000); // Max zoom out
} }
@@ -107,6 +106,15 @@ async function openGithub() {
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank') 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 // Add keyboard shortcuts
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
@@ -118,35 +126,35 @@ window.addEventListener('keydown', (event) => {
</script> </script>
<template> <template>
<orientation-gizmo :scene="props.viewer.scene as any" :elem="props.viewer.elem" v-if="props.viewer?.scene"/> <orientation-gizmo v-if="props.viewer?.scene" :viewer="props.viewer"/>
<v-divider/> <v-divider/>
<h5>Camera</h5> <h5>Camera</h5>
<v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span> <v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span>
<v-tooltip activator="parent">Toggle (P)rojection<br/>(currently <v-tooltip activator="parent">Toggle (P)rojection<br/>(currently
{{ toggleProjectionText === 'PERSP' ? 'perspective' : 'orthographic' }}) {{ toggleProjectionText === 'PERSP' ? 'perspective' : 'orthographic' }})
</v-tooltip> </v-tooltip>
<svg-icon type="mdi" :path="mdiProjector"></svg-icon> <svg-icon :path="mdiProjector" type="mdi"></svg-icon>
</v-btn> </v-btn>
<v-btn icon @click="centerCamera"> <v-btn icon @click="centerCamera">
<v-tooltip activator="parent">Re(c)enter Camera</v-tooltip> <v-tooltip activator="parent">Re(c)enter Camera</v-tooltip>
<svg-icon type="mdi" :path="mdiCrosshairsGps"/> <svg-icon :path="mdiCrosshairsGps" type="mdi"/>
</v-btn> </v-btn>
<v-divider/> <v-divider/>
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5> <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" v-model="selection" :viewer="props.viewer as any"
@findModel="(name) => emit('findModel', name)"/> @findModel="(name) => emit('findModel', name)"/>
<v-divider/> <v-divider/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<h5>Extras</h5> <h5>Extras</h5>
<v-btn icon @click="downloadSceneGlb"> <v-btn icon @click="downloadSceneGlb">
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip> <v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
<svg-icon type="mdi" :path="mdiDownload"/> <svg-icon :path="mdiDownload" type="mdi"/>
</v-btn> </v-btn>
<v-dialog id="licenses-dialog" fullscreen> <v-dialog id="licenses-dialog" fullscreen>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn icon v-bind="props"> <v-btn icon v-bind="props">
<v-tooltip activator="parent">Show Licenses</v-tooltip> <v-tooltip activator="parent">Show Licenses</v-tooltip>
<svg-icon type="mdi" :path="mdiLicense"/> <svg-icon :path="mdiLicense" type="mdi"/>
</v-btn> </v-btn>
</template> </template>
<template v-slot:default="{ isActive }"> <template v-slot:default="{ isActive }">
@@ -156,7 +164,7 @@ window.addEventListener('keydown', (event) => {
<v-spacer> <v-spacer>
</v-spacer> </v-spacer>
<v-btn icon @click="isActive.value = false"> <v-btn icon @click="isActive.value = false">
<svg-icon type="mdi" :path="mdiClose"/> <svg-icon :path="mdiClose" type="mdi"/>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
<v-card-text> <v-card-text>
@@ -167,7 +175,7 @@ window.addEventListener('keydown', (event) => {
</v-dialog> </v-dialog>
<v-btn icon @click="openGithub"> <v-btn icon @click="openGithub">
<v-tooltip activator="parent">Open (G)itHub</v-tooltip> <v-tooltip activator="parent">Open (G)itHub</v-tooltip>
<svg-icon type="mdi" :path="mdiGithub"/> <svg-icon :path="mdiGithub" type="mdi"/>
</v-btn> </v-btn>
<div ref="statsHolder"></div> <div ref="statsHolder"></div>
</template> </template>
@@ -187,4 +195,8 @@ window.addEventListener('keydown', (event) => {
position: relative; position: relative;
top: 5px; top: 5px;
} }
h5 {
font-size: 14px;
}
</style> </style>

163
frontend/tools/selection.ts Normal file
View 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()];
}

View File

@@ -1,41 +1,115 @@
<script setup lang="ts"> <script lang="ts" setup>
import {settings} from "../misc/settings"; import {settings} from "../misc/settings";
import {inject, onMounted, type Ref, ref, watch} from "vue"; 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 {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
import Loading from "../misc/Loading.vue"; import {$controls} from '@google/model-viewer/lib/features/controls.js';
import {type SmoothControls} from '@google/model-viewer/lib/three-components/SmoothControls';
import {ModelViewerElement} from '@google/model-viewer'; import {ModelViewerElement} from '@google/model-viewer';
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot"; import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
import type {Vector3} from "three"; 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 ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
//@ts-ignore
const emit = defineEmits<{ load: [] }>() BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
//@ts-ignore
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
//@ts-ignore
Mesh.prototype.raycast = acceleratedRaycast;
const props = defineProps<{ src: string }>(); const props = defineProps<{ src: string }>();
const elem = ref<ModelViewerElement | null>(null); const elem = ref<ModelViewerElement | null>(null);
const scene = ref<ModelScene | null>(null); const scene = ref<ModelScene | null>(null);
const renderer = ref<Renderer | null>(null); const renderer = ref<Renderer | null>(null);
const controls = ref<SmoothControls | null>(null);
let lastCameraTargetPosition: Vector3 | undefined = undefined;
let lastCameraZoom: number | undefined = undefined;
let lastCameraUrl = props.src.toString();
onMounted(() => { onMounted(() => {
if (!elem.value) return; if (!elem.value) return;
elem.value.addEventListener('load', async () => { elem.value.addEventListener('before-render', () => {
if (!elem.value) return; if (!elem.value) return;
// Delete the initial load banner // Extract internals of model-viewer in order to hack unsupported features
let banner = elem.value.querySelector('.initial-load-banner');
if (banner) banner.remove();
// Set the scene and renderer
scene.value = elem.value[$scene] as ModelScene; scene.value = elem.value[$scene] as ModelScene;
renderer.value = elem.value[$renderer] as Renderer; renderer.value = elem.value[$renderer] as Renderer;
// Emit the load event controls.value = (elem.value as any)[$controls] as SmoothControls;
emit('load') // Recover the camera position if it was set before
if (lastCameraTargetPosition) {
// console.log("RESTORING camera position?", lastCameraTargetPosition);
scene.value.setTarget(-lastCameraTargetPosition.x, -lastCameraTargetPosition.y, -lastCameraTargetPosition.z);
scene.value.jumpToGoal(); // Avoid move animation
}
(async () => {
let tries = 0
while (tries++ < 25) {
if (!lastCameraZoom || !elem.value?.getCameraOrbit()?.radius) break;
let change = lastCameraZoom - elem.value.getCameraOrbit().radius;
//console.log("Zooming to", lastCameraZoom, "from", elem.value.getCameraOrbit().radius, "change", change);
if (Math.abs(change) < 0.001) break;
elem.value.zoom(-Math.sign(change) * (Math.pow(Math.abs(change) + 1, 0.9) - 1)); // Arbitrary, experimental
elem.value.jumpCameraToGoal();
await elem.value.updateComplete;
}
//console.log("Ready to save!")
lastCameraUrl = props.src.toString();
})();
}); });
elem.value.addEventListener('camera-change', onCameraChange); elem.value.addEventListener('camera-change', onCameraChange);
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
}); });
function onCameraChange() {
// Remember the camera position to keep it in case of scene changes
if (scene.value && props.src.toString() == lastCameraUrl) { // Don't overwrite with initial unwanted positions
lastCameraTargetPosition = scene.value.target.position.clone();
lastCameraZoom = elem.value?.getCameraOrbit()?.radius;
//console.log("Saving camera?", lastCameraTargetPosition, lastCameraZoom);
}
// Also need to update the SVG overlay
for (let lineId in lines.value) {
onCameraChangeLine(lineId as any);
}
}
// 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);
};
const poster = ref<string>("")
const setPosterText = (newText: string) => {
poster.value = "data:image/svg+xml;charset=utf-8;base64," + btoa(
'<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg" fill="gray">' +
'<text x="50%" y="0%" dominant-baseline="middle" text-anchor="middle" font-size="48px">' +
newText +
'</text>' +
'</svg>')
}
setPosterText("Loading...")
class Line3DData { class Line3DData {
startHotspot: HTMLElement = document.body startHotspot: HTMLElement = document.body
endHotspot: HTMLElement = document.body endHotspot: HTMLElement = document.body
@@ -88,13 +162,6 @@ function removeLine3D(id: number): boolean {
return true; return true;
} }
function onCameraChange() {
// Need to update the SVG overlay
for (let lineId in lines.value) {
onCameraChangeLine(lineId as any);
}
}
let svg = ref<SVGElement | null>(null); let svg = ref<SVGElement | null>(null);
function onCameraChangeLine(lineId: number) { function onCameraChangeLine(lineId: number) {
@@ -130,48 +197,47 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
return Object.entries(lines); return Object.entries(lines);
} }
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D}); defineExpose({elem, onElemReady, scene, renderer, controls, addLine3D, removeLine3D, onProgress, setPosterText});
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!; let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
watch(disableTap, (value) => { watch(disableTap, (newDisableTap) => {
// Rerender not auto triggered? This works anyway... if (elem.value) elem.value.disableTap = newDisableTap;
if (value) elem.value?.setAttribute('disable-tap', '');
else elem.value?.removeAttribute('disable-tap');
}); });
</script> </script>
<template> <template>
<!-- The main 3D model viewer --> <!-- The main 3D model viewer -->
<model-viewer ref="elem" style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls <model-viewer ref="elem" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :autoplay="settings.autoplay"
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto" :environment-image="settings.background" :exposure="settings.exposure"
min-camera-orbit="-Infinity 0deg 5%" :disable-tap="disableTap" :exposure="settings.exposure" :orbit-sensitivity="settings.orbitSensitivity" :pan-sensitivity="settings.panSensitivity"
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay" :poster="poster" :shadow-intensity="settings.shadowIntensity" :skybox-image="settings.background"
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background" :src="props.src" :zoom-sensitivity="settings.zoomSensitivity" alt="The 3D model(s)" camera-controls
:environment-image="settings.background"> camera-orbit="30deg 75deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
<slot></slot> <!-- Controls, annotations, etc. --> min-camera-orbit="-Infinity 0deg 5%" style="width: 100%; height: 100%">
<div class="annotation initial-load-banner"> <slot></slot>
Trying to load models from... <!-- Add a progress bar to the top of the model viewer -->
<v-list v-for="src in settings.preload" :key="src"> <div ref="progressBar" slot="progress-bar" class="progress-bar">
<v-list-item>{{ src }}</v-list-item> <div ref="updateBar" class="update-bar"/>
</v-list>
<loading></loading>
</div> </div>
</model-viewer> </model-viewer>
<!-- The SVG overlay for fake 3D lines attached to the model --> <!-- The SVG overlay for fake 3D lines attached to the model -->
<div class="overlay-svg-wrapper"> <div class="overlay-svg-wrapper">
<svg ref="svg" class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"> <svg ref="svg" class="overlay-svg" height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
<g v-for="[lineId, line] in entries(lines)" :key="lineId"> <g v-for="[lineId, line] in entries(lines)" :key="lineId">
<line :x1="line.start2D[0]" :y1="line.start2D[1]" :x2="line.end2D[0]" <line :x1="line.start2D[0]" :x2="line.end2D[0]" :y1="line.start2D[1]"
:y2="line.end2D[1]" v-bind="line.lineAttrs"/> :y2="line.end2D[1]" v-bind="line.lineAttrs"/>
<g v-if="line.centerText !== undefined"> <g v-if="line.centerText !== undefined">
<rect :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4" <rect v-if="line.centerText"
:y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2" :height="line.centerTextSize[1] + 4"
:width="line.centerTextSize[0] + 8" :height="line.centerTextSize[1] + 4" :width="line.centerTextSize[0] + 8"
fill="gray" fill-opacity="0.75" rx="4" ry="4" stroke="black" v-if="line.centerText"/> :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"
<text :x="(line.start2D[0] + line.end2D[0]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2" fill="gray"
text-anchor="middle" dominant-baseline="middle" font-size="16" fill="black" fill-opacity="0.75" rx="4" ry="4" stroke="black"/>
:class="'line' + lineId + '_text'" v-if="line.centerText"> <text v-if="line.centerText" :class="'line' + lineId + '_text'"
:x="(line.start2D[0] + line.end2D[0]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2"
dominant-baseline="middle" fill="black"
font-size="16" text-anchor="middle">
{{ line.centerText }} {{ line.centerText }}
</text> </text>
</g> </g>
@@ -206,14 +272,29 @@ watch(disableTap, (value) => {
pointer-events: none; pointer-events: none;
} }
.initial-load-banner { .progress-bar {
width: 300px; display: block;
margin: auto; pointer-events: none;
margin-top: 3em; width: 100%;
overflow: hidden; 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;
} }
.initial-load-banner .v-list-item { .update-bar {
overflow: hidden; background-color: rgba(255, 255, 255, 0.9);
width: 0;
height: 100%;
border-radius: 25px;
float: left;
transition: width 0.3s;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
{ {
"name": "yet-another-cad-viewer", "name": "yet-another-cad-viewer",
"version": "0.6.13", "version": "0.8.8",
"description": "", "description": "",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
@@ -15,20 +15,22 @@
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite" "update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
}, },
"dependencies": { "dependencies": {
"@gltf-transform/core": "^3.10.0", "@gltf-transform/core": "^3.10.1",
"@gltf-transform/functions": "^3.10.0", "@gltf-transform/extensions": "^3.10.1",
"@gltf-transform/functions": "^3.10.1",
"@google/model-viewer": "^3.4.0", "@google/model-viewer": "^3.4.0",
"@jamescoyle/vue-icon": "^0.1.2", "@jamescoyle/vue-icon": "^0.1.2",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47", "@mdi/svg": "^7.4.47",
"three": "^0.160.1", "three": "^0.160.1",
"three-mesh-bvh": "^0.7.3",
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo", "three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
"vue": "^3.4.21", "vue": "^3.4.21",
"vuetify": "^3.5.8" "vuetify": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node20": "^20.1.2", "@tsconfig/node20": "^20.1.4",
"@types/node": "^20.11.25", "@types/node": "^20.12.2",
"@types/three": "^0.160.0", "@types/three": "^0.160.0",
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -37,9 +39,9 @@
"commander": "^12.0.0", "commander": "^12.0.0",
"generate-license-file": "^3.0.1", "generate-license-file": "^3.0.1",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"terser": "^5.29.1", "terser": "^5.30.0",
"typescript": "~5.4.2", "typescript": "~5.4.3",
"vite": "^5.1.5", "vite": "^5.2.7",
"vue-tsc": "^2.0.6" "vue-tsc": "^2.0.7"
} }
} }

135
poetry.lock generated
View File

@@ -34,12 +34,12 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]] [[package]]
name = "build123d" name = "build123d"
version = "0.4.0" version = "0.5.0"
description = "A python CAD programming library" description = "A python CAD programming library"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ 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] [package.dependencies]
@@ -50,7 +50,7 @@ ipython = ">=8.0.0,<9"
numpy = ">=1.24.1,<2" numpy = ">=1.24.1,<2"
numpy-stl = ">=3.0.0,<4" numpy-stl = ">=3.0.0,<4"
ocpsvg = "*" ocpsvg = "*"
py-lib3mf = "*" py-lib3mf = ">=2.3.1"
svgpathtools = ">=1.5.1,<2" svgpathtools = ">=1.5.1,<2"
trianglesolver = "*" trianglesolver = "*"
typing-extensions = ">=4.6.0,<5" typing-extensions = ">=4.6.0,<5"
@@ -218,53 +218,53 @@ draw5 = ["Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib"]
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.49.0" version = "4.50.0"
description = "Tools to manipulate font files" description = "Tools to manipulate font files"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"}, {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"},
{file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"}, {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"},
{file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"}, {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"},
{file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"}, {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"},
{file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"}, {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"},
{file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"}, {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"},
{file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"}, {file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"},
{file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"}, {file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"},
{file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"}, {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"},
{file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"}, {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"},
{file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"}, {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"},
{file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"}, {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"},
{file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"}, {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"},
{file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"}, {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"},
{file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"}, {file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"},
{file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"}, {file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"},
{file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"}, {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"},
{file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"}, {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"},
{file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"}, {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"},
{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.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.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"}, {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"},
{file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"}, {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"},
{file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"}, {file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"},
{file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"}, {file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"},
{file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"}, {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"},
{file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"}, {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"},
{file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"}, {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"},
{file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"}, {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"},
{file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"}, {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"},
{file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"}, {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"},
{file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"}, {file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"},
{file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"}, {file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"},
{file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"}, {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"},
{file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"}, {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"},
{file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"}, {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"},
{file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"}, {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"},
{file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"}, {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"},
{file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"}, {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"},
{file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"}, {file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"},
{file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"}, {file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"},
{file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"}, {file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"},
{file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"}, {file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"},
] ]
[package.extras] [package.extras]
@@ -318,17 +318,6 @@ qtconsole = ["qtconsole"]
test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] 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"] 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]] [[package]]
name = "jedi" name = "jedi"
version = "0.19.1" version = "0.19.1"
@@ -350,13 +339,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]] [[package]]
name = "marshmallow" name = "marshmallow"
version = "3.21.0" version = "3.21.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes." description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "marshmallow-3.21.0-py3-none-any.whl", hash = "sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"}, {file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"},
{file = "marshmallow-3.21.0.tar.gz", hash = "sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b"}, {file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"},
] ]
[package.dependencies] [package.dependencies]
@@ -473,13 +462,13 @@ dev = ["pytest"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "24.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
] ]
[[package]] [[package]]
@@ -637,12 +626,12 @@ tests = ["pytest"]
[[package]] [[package]]
name = "py-lib3mf" name = "py-lib3mf"
version = "2.2.0" version = "2.3.1"
description = "A python package for Lib3MF tools" description = "A python package for Lib3MF tools"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ 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]] [[package]]
@@ -676,13 +665,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "3.1.1" version = "3.1.2"
description = "pyparsing module - Classes and methods to define and execute parsing grammars" description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false optional = false
python-versions = ">=3.6.8" python-versions = ">=3.6.8"
files = [ files = [
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
] ]
[package.extras] [package.extras]
@@ -819,18 +808,18 @@ files = [
[[package]] [[package]]
name = "traitlets" name = "traitlets"
version = "5.14.1" version = "5.14.2"
description = "Traitlets Python configuration system" description = "Traitlets Python configuration system"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"},
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"},
] ]
[package.extras] [package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 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]] [[package]]
name = "trianglesolver" name = "trianglesolver"
@@ -962,4 +951,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "d9746e99dd8861758730e68d12dc72d9ec5fb0101b3c070a7d7a373439c658a0" content-hash = "612c2f4fcc3ff9e37bc9e604bf092452138843ec4dc529dadc210887f0e728fd"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "yacv-server" name = "yacv-server"
version = "0.6.13" version = "0.8.8"
description = "Yet Another CAD Viewer (server)" description = "Yet Another CAD Viewer (server)"
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"] authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
license = "MIT" license = "MIT"
@@ -14,12 +14,11 @@ include = [
python = "^3.9" python = "^3.9"
# CAD # CAD
build123d = "^0.4.0" build123d = "^0.5.0"
# Misc # Misc
pygltflib = "^1.16.2" pygltflib = "^1.16.2"
pillow = "^10.2.0" pillow = "^10.2.0"
iterators = "^0.2.0"
[tool.poetry.build] [tool.poetry.build]
generate-setup-file = false generate-setup-file = false

View File

@@ -26,8 +26,10 @@ export default defineConfig({
} }
}, },
build: { build: {
assetsDir: '.', assetsDir: '.', // Support deploying to a subdirectory using relative URLs
cssCodeSplit: false, // Small enough to inline 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: { define: {
__APP_NAME__: JSON.stringify(name), __APP_NAME__: JSON.stringify(name),

View File

@@ -15,7 +15,7 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
# Expose some nice aliases using the default server instance # Expose some nice aliases using the default server instance
show = yacv.show show = yacv.show
show_all = yacv.show_cad_all show_all = yacv.show_cad_all
prepare_image = image_to_gltf image_to_gltf = image_to_gltf
export_all = yacv.export_all export_all = yacv.export_all
remove = yacv.remove remove = yacv.remove
clear = yacv.clear clear = yacv.clear

View File

@@ -2,10 +2,15 @@
Utilities to work with CAD objects Utilities to work with CAD objects
""" """
import hashlib 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.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from build123d import Compound, Shape
from yacv_server.gltf import GLTFMgr 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): if isinstance(obj, TopoDS_Shape):
return obj 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: if error:
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)') raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
else: else:
return None 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 """ """ Grab all shapes by inspecting the stack """
import inspect import inspect
stack = inspect.stack() stack = inspect.stack()
shapes = [] shapes = set()
for frame in stack: for frame in stack:
for key, value in frame.frame.f_locals.items(): for key, value in frame.frame.f_locals.items():
shape = get_shape(value, error=False) shape = get_shape(value, error=False)
if shape: if shape and shape not in shapes:
shapes.append((key, shape)) shapes.add((key, shape))
return shapes return shapes
def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None, 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.""" """Convert an image to a GLTF CAD object."""
from PIL import Image from PIL import Image
import io 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): if not isinstance(center_loc, TopLoc_Location):
raise ValueError('Center location not valid') raise ValueError('Center location not valid')
plane = Plane(Location(center_loc)) 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 # Load the image to a byte buffer
img = Image.open(source) img = Image.open(source)
img_buf = io.BytesIO() img_buf = io.BytesIO()
img.save(img_buf, format=format)
img_buf = img_buf.getvalue()
# Build the gltf # Use the original dimensions for scaling the model
mgr = GLTFMgr(image=(img_buf, save_mime))
if width is None and height is None: if width is None and height is None:
raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm? raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm?
elif width is None: elif width is None:
width = img.width / img.height * height width = img.width / img.height * height
elif height is None: elif height is None:
height = height or img.height / img.width * width # Apply default aspect ratio if unspecified 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([ 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),
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
], [ ], [
(0, 2, 1), (0, 2, 1),
(0, 3, 2), (0, 3, 2),
@@ -125,7 +158,36 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
(1, 0), (1, 0),
(1, 1), (1, 1),
(0, 1), (0, 1),
]) ], (1, 1, 1, 1))
# Return the GLTF binary blob and the suggested name of the image # 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()

View File

@@ -12,43 +12,95 @@ _checkerboard_image_bytes = base64.decodebytes(
class GLTFMgr: class GLTFMgr:
"""A utility class to build our GLTF2 objects easily and incrementally""" """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( self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"), asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
scene=0, scene=0,
scenes=[Scene(nodes=[0])], scenes=[Scene(nodes=[0])],
nodes=[Node(mesh=0)], nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
meshes=[Mesh(primitives=[])], meshes=[Mesh(primitives=[
accessors=[], Primitive(indices=-1, attributes=Attributes(), mode=TRIANGLES, material=0,
bufferViews=[BufferView(buffer=0, byteLength=len(image[0]), byteOffset=0)], extras={"face_triangles_end": []}),
buffers=[Buffer(byteLength=len(image[0]))], Primitive(indices=-1, attributes=Attributes(), mode=LINES, material=0,
samplers=[Sampler(magFilter=NEAREST)], extras={"edge_points_end": []}),
textures=[Texture(source=0, sampler=0)], Primitive(indices=-1, attributes=Attributes(), mode=POINTS, material=0),
images=[Image(bufferView=0, mimeType=image[1])], ])],
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]], @property
tex_coord_raw: List[Tuple[float, float]]): def _faces_primitive(self) -> Primitive:
"""Add a face to the GLTF as a new primitive of the unique mesh""" return [p for p in self.gltf.meshes[0].primitives if p.mode == TRIANGLES][0]
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")
def add_edge(self, vertices_raw: List[Tuple[float, float, float]], mat: str = None): @property
"""Add an edge to the GLTF as a new primitive of the unique mesh""" def _edges_primitive(self) -> Primitive:
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32) return [p for p in self.gltf.meshes[0].primitives if p.mode == LINES][0]
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")
def add_vertex(self, vertex: Tuple[float, float, float]): @property
"""Add a vertex to the GLTF as a new primitive of the unique mesh""" def _vertices_primitive(self) -> Primitive:
vertices = np.array([[vertex[0], vertex[1], vertex[2]]]) return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
indices = np.array([[0]], dtype=np.uint32)
tex_coord = np.array([], dtype=np.float32) def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex") 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): def add_location(self, loc: Location):
"""Add a location to the GLTF as a new primitive of the unique mesh""" """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 # 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_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.x_dir))], color=(0.97, 0.24, 0.24, 1))
self.add_edge([vert(pl.origin), vert(pl.origin + pl.y_dir)], mat="locY") 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)], mat="locZ") 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: def build(self) -> GLTF2:
"""It is important to use a different material for each primitive to be able to change them at runtime""" """Merge the intermediate data into the GLTF object and return it"""
new_material: Material buffers_list: List[Tuple[Accessor, BufferView, bytes]] = []
if kind == "face":
new_material = Material(name="face", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( if len(self.face_indices) > 0:
baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 1, 0.5, 1]), doubleSided=True) self._faces_primitive.indices = len(buffers_list)
elif kind == "edge": buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
new_material = Material(name="edge", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( self._faces_primitive.attributes.POSITION = len(buffers_list)
baseColorFactor=[0, 0, 0.5, 1])) buffers_list.append(_gen_buffer_metadata(self.face_positions, 3))
elif kind == "vertex": self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
baseColorFactor=[0, 0.3, 0.3, 1])) self._faces_primitive.attributes.COLOR_0 = len(buffers_list)
elif kind == "locX": buffers_list.append(_gen_buffer_metadata(self.face_colors, 4))
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]))
else: else:
raise ValueError(f"Unknown material kind {kind}") self.image = None # Unused image
self.gltf.materials.append(new_material) self.gltf.meshes[0].primitives = list( # Remove unused faces primitive
return len(self.gltf.materials) - 1 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, edges_and_vertices_mat = 0
material: str = "face"): if self.image is not None and (len(self.edge_indices) > 0 or len(self.vertex_indices) > 0):
assert vertices.ndim == 2 # Create a material without texture for edges and vertices
assert vertices.shape[1] == 3 edges_and_vertices_mat = len(self.gltf.materials)
vertices = vertices.astype(np.float32) new_mat = copy.deepcopy(self.gltf.materials[0])
vertices_blob = vertices.tobytes() new_mat.pbrMetallicRoughness.baseColorTexture = None
self.gltf.materials.append(new_mat)
assert indices.ndim == 2 # Treat edges and vertices the same way
assert indices.shape[1] == 3 and mode == TRIANGLES or indices.shape[1] == 2 and mode == LINE_STRIP or \ for (indices, positions, colors, primitive, kind) in [
indices.shape[1] == 1 and mode == POINTS (self.edge_indices, self.edge_positions, self.edge_colors, self._edges_primitive, LINES),
indices = indices.astype(np.uint32) (self.vertex_indices, self.vertex_positions, self.vertex_colors, self._vertices_primitive, POINTS)
indices_blob = indices.flatten().tobytes() ]:
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 if self.image is not None: # Add texture last as it creates a fake accessor that is not added!
# This can happen on broken faces like on some fonts self.gltf.images = [Image(bufferView=len(buffers_list), mimeType=self.image[1])]
# assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}" self.gltf.textures = [Texture(source=0, sampler=0)]
# assert indices.min() == 0, f"min({indices}) != 0" self.gltf.samplers = [Sampler(magFilter=NEAREST)]
# assert np.unique(indices.flatten()).size == len(vertices) 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 # Once all the data is ready, we can concatenate the buffers updating the accessors and views
assert len(tex_coord) == 0 or tex_coord.shape[1] == 2 prev_binary_blob = self.gltf.binary_blob() or b''
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()
byte_offset_base = len(prev_binary_blob) byte_offset_base = len(prev_binary_blob)
self.gltf.bufferViews.extend([bv for bv in [ for accessor, bufferView, blob in buffers_list:
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])
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()

View File

@@ -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') Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
extrude(amount=1) extrude(amount=1)
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location # Avoid overlapping: logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location
logo_img_location.position = Vector(logo_img_location.position.X - 4e-2, logo_img_location.position.Y, logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
logo_img_location.position.Z)
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg') 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() 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__": 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 # 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' 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 # Build the CAD part of the logo
logo = build_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()]) show(*[obj for obj in logo.values()], names=[name for name in logo.keys()])
if testing_server: if testing_server:
remove('location') # Test removing a part # remove('location') # Test removing a part
pass
else: else:
# Save the complete logo to multiple GLB files # Save the complete logo to multiple GLB files
export_all(os.path.join(ASSETS_DIR, 'logo_build')) export_all(os.path.join(ASSETS_DIR, 'logo_build'))

View File

@@ -1,12 +1,9 @@
import io import io
import os import os
import threading
import urllib.parse import urllib.parse
from http import HTTPStatus from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler from http.server import SimpleHTTPRequestHandler
from iterators import TimeoutIterator
from yacv_server.mylogger import logger from yacv_server.mylogger import logger
# Find the frontend folder (optional, but recommended) # Find the frontend folder (optional, but recommended)
@@ -26,13 +23,9 @@ OBJECTS_API_PATH = '/api/object' # /{name}
class HTTPHandler(SimpleHTTPRequestHandler): class HTTPHandler(SimpleHTTPRequestHandler):
yacv: 'yacv.YACV' 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): def __init__(self, *args, yacv: 'yacv.YACV', **kwargs):
self.yacv = yacv self.yacv = yacv
self.frontend_lock = threading.Lock()
self.at_least_one_client = threading.Event()
super().__init__(*args, **kwargs, directory=FRONTEND_BASE_PATH) super().__init__(*args, **kwargs, directory=FRONTEND_BASE_PATH)
def log_message(self, fmt, *args): def log_message(self, fmt, *args):
@@ -77,14 +70,23 @@ class HTTPHandler(SimpleHTTPRequestHandler):
def _api_updates(self): def _api_updates(self):
"""Handles a publish-only websocket connection that send show_object events along with their hashes and URLs""" """Handles a publish-only websocket connection that send show_object events along with their hashes and URLs"""
# Keep a shared read lock to know if any frontend is still working before shutting down
with self.yacv.frontend_lock.r_locked():
# 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')
self.send_response(HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/event-stream") self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache") self.send_header("Cache-Control", "no-cache")
# Chunked transfer encoding! # Chunked transfer encoding!
self.send_header("Transfer-Encoding", "chunked") self.send_header("Transfer-Encoding", "chunked")
self.end_headers() self.end_headers()
self.at_least_one_client.set()
logger.debug('Updates client connected')
def write_chunk(_chunk_data: str): def write_chunk(_chunk_data: str):
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8')) self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
@@ -95,20 +97,9 @@ class HTTPHandler(SimpleHTTPRequestHandler):
write_chunk('retry: 100\n\n') write_chunk('retry: 100\n\n')
# Send buffered events first, while keeping a lock subscription = self.yacv.show_events.subscribe(yield_timeout=1.0) # Keep-alive interval
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')
# 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: try:
for data in it: for data in subscription:
if data is None: if data is None:
write_chunk(':keep-alive\n\n') write_chunk(':keep-alive\n\n')
else: else:
@@ -119,27 +110,25 @@ class HTTPHandler(SimpleHTTPRequestHandler):
except BrokenPipeError: # Client disconnected normally except BrokenPipeError: # Client disconnected normally
pass pass
finally: finally:
logger.debug('Updates client disconnected')
try:
it.interrupt()
next(it) # Make sure the iterator is interrupted before trying to close the subscription
subscription.close() 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): def _api_object(self, obj_name: str):
"""Returns the object file with the matching name, building it if necessary.""" """Returns the object file with the matching name, building it if necessary."""
with self.frontend_lock:
# Export the object (or fail if not found) # Export the object (or fail if not found)
exported_glb = self.yacv.export(obj_name) _export = self.yacv.export(obj_name)
if exported_glb is None: if _export is None:
self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found') self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found')
return io.BytesIO() return io.BytesIO()
exported_glb, _hash = _export
# Wrap the GLB in a response and return it # Wrap the GLB in a response and return it
self.send_response(HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'model/gltf-binary') self.send_header('Content-Type', 'model/gltf-binary')
self.send_header('Content-Length', str(len(exported_glb))) self.send_header('Content-Length', str(len(exported_glb)))
self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"') self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"')
self.send_header('E-Tag', f'"{_hash}"')
self.end_headers() self.end_headers()
self.wfile.write(exported_glb) self.wfile.write(exported_glb)

View File

@@ -1,4 +1,4 @@
import threading import queue
import queue import queue
import threading import threading
from typing import List, TypeVar, \ from typing import List, TypeVar, \
@@ -8,6 +8,8 @@ from yacv_server.mylogger import logger
T = TypeVar('T') T = TypeVar('T')
_end_of_queue = object()
class BufferedPubSub(Generic[T]): class BufferedPubSub(Generic[T]):
"""A simple implementation of publish-subscribe pattern using threading and buffering all previous events""" """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: for event in self._buffer:
q.put(event) q.put(event)
if not include_future: if not include_future:
q.put(None) q.put(_end_of_queue)
return q return q
def _unsubscribe(self, q: queue.Queue[T]): def _unsubscribe(self, q: queue.Queue[T]):
@@ -54,14 +56,18 @@ class BufferedPubSub(Generic[T]):
self._subscribers.remove(q) self._subscribers.remove(q)
logger.debug(f"Unsubscribed from %s (%d subscribers)", self, len(self._subscribers)) 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""" """Subscribes to events as an generator that yields events and automatically unsubscribes"""
q = self._subscribe(include_buffered, include_future) q = self._subscribe(include_buffered, include_future)
try: try:
while True: 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 # 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 break
yield v yield v
finally: # When aclose() is called finally: # When aclose() is called

96
yacv_server/rwlock.py Normal file
View 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()

View File

@@ -1,14 +1,9 @@
import hashlib from typing import List, Dict, Tuple
import io
import re
from typing import List, Dict, Tuple, Union
from OCP.BRep import BRep_Tool from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_TangentialDeflection from OCP.GCPnts import GCPnts_TangentialDeflection
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location 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 OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
from build123d import Shape, Vertex, Face, Location from build123d import Shape, Vertex, Face, Location
from pygltflib import GLTF2 from pygltflib import GLTF2
@@ -36,8 +31,8 @@ def tessellate(
shape = Shape(cad_like) shape = Shape(cad_like)
# Perform tessellation tasks # Perform tessellation tasks
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {} edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {} vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
if faces: if faces:
for face in shape.faces(): for face in shape.faces():
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance) _tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
@@ -55,7 +50,7 @@ def tessellate(
for vertex in shape.vertices(): for vertex in shape.vertices():
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, [])) _tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
return mgr.gltf return mgr.build()
def _tessellate_face( def _tessellate_face(
@@ -65,12 +60,12 @@ def _tessellate_face(
angular_tolerance: float = 0.1 angular_tolerance: float = 0.1
): ):
face = Shape(ocp_face) 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()) poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
if poly is None: if poly is None:
logger.warn("No triangulation found for face") logger.warn("No triangulation found for face")
return GLTF2() return GLTF2()
tri_mesh = face.tessellate(tolerance, angular_tolerance)
# Get UV of each face from the parameters # Get UV of each face from the parameters
uv = [ uv = [
@@ -78,7 +73,7 @@ def _tessellate_face(
for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1)) 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] indices = tri_mesh[1]
mgr.add_face(vertices, indices, uv) 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) push_dir = (push_dir[0] + normal.X, push_dir[1] + normal.Y, push_dir[2] + normal.Z)
if push_dir != (0, 0, 0): if push_dir != (0, 0, 0):
# Normalize the push direction by the number of faces and a constant factor # Normalize the push direction by the number of faces and a constant factor
# NOTE: Don't overdo it, or metrics will be wrong # NOTE: Don't overdo it, or metrics will be (more) wrong
n = len(faces) / 1e-3 n = 1e-3 / len(faces)
push_dir = (push_dir[0] / n, push_dir[1] / n, push_dir[2] / n) push_dir = (push_dir[0] * n, push_dir[1] * n, push_dir[2] * n)
# Push the vertex by the normal # Push the vertex by the normal
v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2]) v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2])
return v return v
@@ -119,6 +114,9 @@ def _tessellate_edge(
for i in range(1, discretizer.NbPoints() + 1) 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) 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)) 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()

View File

@@ -10,7 +10,7 @@ from dataclasses import dataclass
from http.server import ThreadingHTTPServer from http.server import ThreadingHTTPServer
from importlib.metadata import version from importlib.metadata import version
from threading import Thread 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.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
@@ -22,7 +22,9 @@ from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
from yacv_server.myhttp import HTTPHandler from yacv_server.myhttp import HTTPHandler
from yacv_server.mylogger import logger from yacv_server.mylogger import logger
from yacv_server.pubsub import BufferedPubSub 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 @dataclass_json
@@ -33,8 +35,8 @@ class UpdatesApiData:
"""Name of the object. Should be unique unless you want to overwrite the previous object""" """Name of the object. Should be unique unless you want to overwrite the previous object"""
hash: str hash: str
"""Hash of the object, to detect changes without rebuilding the object""" """Hash of the object, to detect changes without rebuilding the object"""
is_remove: bool is_remove: Optional[bool]
"""Whether to remove the object from the scene""" """Whether to remove the object from the scene. If None, this is a shutdown request"""
YACVSupported = Union[bytes, CADCoreLike] YACVSupported = Union[bytes, CADCoreLike]
@@ -46,7 +48,7 @@ class UpdatesApiFullData(UpdatesApiData):
kwargs: Optional[Dict[str, any]] kwargs: Optional[Dict[str, any]]
"""The show_object options, if any (not serialized)""" """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): kwargs: Optional[Dict[str, any]] = None):
self.name = name self.name = name
self.hash = _hash self.hash = _hash
@@ -60,22 +62,42 @@ class UpdatesApiFullData(UpdatesApiData):
class YACV: class YACV:
"""The main yacv_server class, which manages the web server and the CAD objects."""
# Startup
server_thread: Optional[Thread] server_thread: Optional[Thread]
"""The main thread running the server (will spawn other threads for each request)"""
server: Optional[ThreadingHTTPServer] server: Optional[ThreadingHTTPServer]
"""The server object"""
startup_complete: threading.Event startup_complete: threading.Event
"""Event to signal when the server has started"""
# Running
show_events: BufferedPubSub[UpdatesApiFullData] show_events: BufferedPubSub[UpdatesApiFullData]
"""PubSub for show events (objects to be shown in/removed from the scene)"""
build_events: Dict[str, BufferedPubSub[bytes]] 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): def __init__(self):
self.server_thread = None self.server_thread = None
self.server = None self.server = None
self.startup_complete = threading.Event() self.startup_complete = threading.Event()
self.at_least_one_client = threading.Event()
self.show_events = BufferedPubSub() self.show_events = BufferedPubSub()
self.build_events = {} self.build_events = {}
self.object_events_lock = threading.Lock() self.build_events_lock = threading.Lock()
self.frontend_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')) logger.info('Using yacv-server v%s', version('yacv-server'))
def start(self): def start(self):
@@ -100,36 +122,34 @@ class YACV:
logger.error('Cannot stop server because it is not running') logger.error('Cannot stop server because it is not running')
return 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_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0))
graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_REQUEST', 5.0)) if graceful_secs_connect > 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() start = time.time()
try: try:
while not self.at_least_one_client.wait( if not self.at_least_one_client.is_set():
graceful_secs_connect / 10) and time.time() - start < graceful_secs_connect: 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) time.sleep(0.01)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
logger.info('Stopping server (waiting for no more frontend requests)...') # Wait for the server to stop gracefully (all frontends to stop working)
start = time.time() graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_WORK', 1000000))
try: with self.frontend_lock.w_locked(timeout=graceful_secs_request):
while time.time() - start < graceful_secs_request: # Stop the server
if self.frontend_lock.locked():
start = time.time()
time.sleep(0.01)
except KeyboardInterrupt:
pass
# Stop the server in the background
self.server.shutdown() self.server.shutdown()
logger.info('Stopping server (sent)...')
# Wait for the server to stop gracefully # Wait for the server thread to stop
self.server_thread.join(timeout=30) self.server_thread.join(timeout=30)
self.server_thread = None self.server_thread = None
logger.info('Stopping server (confirmed)...')
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM): if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
sys.exit(0) # Exit with success sys.exit(0) # Exit with success
@@ -175,7 +195,7 @@ class YACV:
def show_cad_all(self, **kwargs): def show_cad_all(self, **kwargs):
"""Publishes all CAD objects in the current scope to the server""" """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) self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
def remove(self, name: str): def remove(self, name: str):
@@ -187,7 +207,7 @@ class YACV:
self.show_events.delete(old_show_event) self.show_events.delete(old_show_event)
# Delete any cached object builds # Delete any cached object builds
with self.object_events_lock: with self.build_events_lock:
if name in self.build_events: if name in self.build_events:
del self.build_events[name] del self.build_events[name]
@@ -206,13 +226,13 @@ class YACV:
def shown_object_names(self, apply_removes: bool = True) -> List[str]: def shown_object_names(self, apply_removes: bool = True) -> List[str]:
"""Returns the names of all objects that have been shown""" """Returns the names of all objects that have been shown"""
res = [] res = set()
for obj in self.show_events.buffer(): for obj in self.show_events.buffer():
if not obj.is_remove or not apply_removes: if not obj.is_remove or not apply_removes:
res.append(obj.name) res.add(obj.name)
else: else:
res.remove(obj.name) res.discard(obj.name)
return res return list(res)
def _show_events(self, name: str, apply_removes: bool = True) -> List[UpdatesApiFullData]: def _show_events(self, name: str, apply_removes: bool = True) -> List[UpdatesApiFullData]:
"""Returns the show events with the given name""" """Returns the show events with the given name"""
@@ -228,8 +248,8 @@ class YACV:
res.remove(old_event) res.remove(old_event)
return res return res
def export(self, name: str) -> Optional[bytes]: def export(self, name: str) -> Optional[Tuple[bytes, str]]:
"""Export the given previously-shown object to a single GLB file, building it if necessary.""" """Export the given previously-shown object to a single GLB blob, building it if necessary."""
start = time.time() start = time.time()
# Check that the object to build exists and grab it if it does # Check that the object to build exists and grab it if it does
@@ -240,7 +260,7 @@ class YACV:
event = events[-1] event = events[-1]
# Use the lock to ensure that we don't build the object twice # 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 there are no object events for this name, we need to build the object
if name not in self.build_events: if name not in self.build_events:
logger.debug('Building object %s with hash %s', name, event.hash) logger.debug('Building object %s with hash %s', name, event.hash)
@@ -259,14 +279,15 @@ class YACV:
edges=event.kwargs.get('edges', True), edges=event.kwargs.get('edges', True),
vertices=event.kwargs.get('vertices', True)) vertices=event.kwargs.get('vertices', True))
glb_list_of_bytes = gltf.save_to_bytes() glb_list_of_bytes = gltf.save_to_bytes()
publish_to.publish(b''.join(glb_list_of_bytes)) glb_bytes = b''.join(glb_list_of_bytes)
logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start, publish_to.publish(glb_bytes)
len(gltf.meshes[0].primitives)) 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 # In either case return the elements of a subscription to the async generator
subscription = self.build_events[name].subscribe() subscription = self.build_events[name].subscribe()
try: try:
return next(subscription) return next(subscription), event.hash
finally: finally:
subscription.close() subscription.close()
@@ -277,7 +298,7 @@ class YACV:
for name in self.shown_object_names(): for name in self.shown_object_names():
if export_filter(name, self._show_events(name)[-1].obj): if export_filter(name, self._show_events(name)[-1].obj):
with open(os.path.join(folder, f'{name}.glb'), 'wb') as f: 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 # noinspection PyUnusedLocal
@@ -305,9 +326,18 @@ _find_var_name_count = 0
def _find_var_name(obj: any, avoid_levels: int = 2) -> 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""" """A hacky way to get a stable name for an object that may change over time"""
global _find_var_name_count global _find_var_name_count
obj_shape = get_shape(obj, error=False) or obj
for frame in inspect.stack()[avoid_levels:]: for frame in inspect.stack()[avoid_levels:]:
for key, value in frame.frame.f_locals.items(): for key, value in frame.frame.f_locals.items():
if value is obj: if get_shape(value, error=False) is obj_shape:
return key return key
_find_var_name_count += 1 _find_var_name_count += 1
return 'unknown_var_' + str(_find_var_name_count) 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
View File

@@ -275,143 +275,143 @@
resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz#dd19fcb8cc6e33ede237fc1b7af96c70833d8f93" resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz#dd19fcb8cc6e33ede237fc1b7af96c70833d8f93"
integrity sha512-GuvZ38d23H+7Tz2C9DhzCepivsOsky03s5NI+KCy7ke1FNUvsJ2oO47scQ9YaGGhgjgNW5OYYNSADmbjcSoIhw== integrity sha512-GuvZ38d23H+7Tz2C9DhzCepivsOsky03s5NI+KCy7ke1FNUvsJ2oO47scQ9YaGGhgjgNW5OYYNSADmbjcSoIhw==
"@esbuild/aix-ppc64@0.19.12": "@esbuild/aix-ppc64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
"@esbuild/android-arm64@0.19.12": "@esbuild/android-arm64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
"@esbuild/android-arm@0.19.12": "@esbuild/android-arm@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
"@esbuild/android-x64@0.19.12": "@esbuild/android-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
"@esbuild/darwin-arm64@0.19.12": "@esbuild/darwin-arm64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
"@esbuild/darwin-x64@0.19.12": "@esbuild/darwin-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
"@esbuild/freebsd-arm64@0.19.12": "@esbuild/freebsd-arm64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
"@esbuild/freebsd-x64@0.19.12": "@esbuild/freebsd-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
"@esbuild/linux-arm64@0.19.12": "@esbuild/linux-arm64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
"@esbuild/linux-arm@0.19.12": "@esbuild/linux-arm@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
"@esbuild/linux-ia32@0.19.12": "@esbuild/linux-ia32@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
"@esbuild/linux-loong64@0.19.12": "@esbuild/linux-loong64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
"@esbuild/linux-mips64el@0.19.12": "@esbuild/linux-mips64el@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
"@esbuild/linux-ppc64@0.19.12": "@esbuild/linux-ppc64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
"@esbuild/linux-riscv64@0.19.12": "@esbuild/linux-riscv64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
"@esbuild/linux-s390x@0.19.12": "@esbuild/linux-s390x@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
"@esbuild/linux-x64@0.19.12": "@esbuild/linux-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
"@esbuild/netbsd-x64@0.19.12": "@esbuild/netbsd-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
"@esbuild/openbsd-x64@0.19.12": "@esbuild/openbsd-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
"@esbuild/sunos-x64@0.19.12": "@esbuild/sunos-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
"@esbuild/win32-arm64@0.19.12": "@esbuild/win32-arm64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
"@esbuild/win32-ia32@0.19.12": "@esbuild/win32-ia32@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
"@esbuild/win32-x64@0.19.12": "@esbuild/win32-x64@0.20.2":
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
"@gltf-transform/core@^3.10.0": "@gltf-transform/core@^3.10.1":
version "3.10.0" version "3.10.1"
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.0.tgz#854e7345f23971e4e7367a29183a2d1b62d45e46" resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
integrity sha512-NxVKhSWvH0j1tjZE8Yl461HUMyZLmYmqcbqHw0TOcQd5Q1SV7Y5w6W68XMt9/amRfMAiJLLNREE7kbr+Z0Ydbw== integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
dependencies: dependencies:
property-graph "^1.3.1" property-graph "^1.3.1"
"@gltf-transform/extensions@^3.10.0": "@gltf-transform/extensions@^3.10.1":
version "3.10.0" version "3.10.1"
resolved "https://registry.yarnpkg.com/@gltf-transform/extensions/-/extensions-3.10.0.tgz#4ae11c3fe8e2a77e6e9dd04ebf0931c7b0cd3690" resolved "https://registry.yarnpkg.com/@gltf-transform/extensions/-/extensions-3.10.1.tgz#71664389cae46fb12eb97dc71eb96d86a0d7801f"
integrity sha512-dz/cf2toBzP+w3ES2VgMiINCN6q86MVGu1lHkT0El4No77Bje9fnHVEPrKwaDCsXi5YXUiG/u6686vK6jePwDA== integrity sha512-xUS9K5fMvW2dkYN4VzxHg2aBPG54M2WqgIjQ7RoSyybMoD7DsPUyMyVgRja+aiTVt/Bxza2ve7zJBD3+tN+aTA==
dependencies: dependencies:
"@gltf-transform/core" "^3.10.0" "@gltf-transform/core" "^3.10.1"
ktx-parse "^0.6.0" ktx-parse "^0.6.0"
"@gltf-transform/functions@^3.10.0": "@gltf-transform/functions@^3.10.1":
version "3.10.0" version "3.10.1"
resolved "https://registry.yarnpkg.com/@gltf-transform/functions/-/functions-3.10.0.tgz#bf0331c109ac948d19be7394d3afcfae84215cfd" resolved "https://registry.yarnpkg.com/@gltf-transform/functions/-/functions-3.10.1.tgz#c40817740241c0ee770f4d1210ccc766e46d8ab2"
integrity sha512-FStbDaH7t2z74RyEeUQn3aBcybULbDkt72ZasC0s7DwQ2DFKKKOth4Zksi4g9+8URNM6vNa2JSfuO851dkJHEg== integrity sha512-Zs6+1qvTD9w40R5qv70E4wJXXacNQ46ZxjKKW6dmfGIyjT8bsSJmV3Tdj+WJ8R6lWXXZ8e2p3ZvAUfPDEG73bQ==
dependencies: dependencies:
"@gltf-transform/core" "^3.10.0" "@gltf-transform/core" "^3.10.1"
"@gltf-transform/extensions" "^3.10.0" "@gltf-transform/extensions" "^3.10.1"
ktx-parse "^0.6.0" ktx-parse "^0.6.0"
ndarray "^1.0.19" ndarray "^1.0.19"
ndarray-lanczos "^0.3.0" ndarray-lanczos "^0.3.0"
@@ -668,70 +668,70 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@rollup/rollup-android-arm-eabi@4.12.0": "@rollup/rollup-android-arm-eabi@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b"
integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w== integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==
"@rollup/rollup-android-arm64@4.12.0": "@rollup/rollup-android-arm64@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e"
integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ== integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==
"@rollup/rollup-darwin-arm64@4.12.0": "@rollup/rollup-darwin-arm64@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b"
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ== integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==
"@rollup/rollup-darwin-x64@4.12.0": "@rollup/rollup-darwin-x64@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455"
integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg== integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==
"@rollup/rollup-linux-arm-gnueabihf@4.12.0": "@rollup/rollup-linux-arm-gnueabihf@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4"
integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA== integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==
"@rollup/rollup-linux-arm64-gnu@4.12.0": "@rollup/rollup-linux-arm64-gnu@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143"
integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA== integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==
"@rollup/rollup-linux-arm64-musl@4.12.0": "@rollup/rollup-linux-arm64-musl@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef"
integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ== integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==
"@rollup/rollup-linux-riscv64-gnu@4.12.0": "@rollup/rollup-linux-riscv64-gnu@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6"
integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw== integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==
"@rollup/rollup-linux-x64-gnu@4.12.0": "@rollup/rollup-linux-x64-gnu@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778"
integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA== integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==
"@rollup/rollup-linux-x64-musl@4.12.0": "@rollup/rollup-linux-x64-musl@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89"
integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw== integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==
"@rollup/rollup-win32-arm64-msvc@4.12.0": "@rollup/rollup-win32-arm64-msvc@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b"
integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw== integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==
"@rollup/rollup-win32-ia32-msvc@4.12.0": "@rollup/rollup-win32-ia32-msvc@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2"
integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA== integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==
"@rollup/rollup-win32-x64-msvc@4.12.0": "@rollup/rollup-win32-x64-msvc@4.13.0":
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10"
integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg== integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==
"@sigstore/bundle@^2.2.0": "@sigstore/bundle@^2.2.0":
version "2.2.0" version "2.2.0"
@@ -777,10 +777,10 @@
"@sigstore/core" "^1.0.0" "@sigstore/core" "^1.0.0"
"@sigstore/protobuf-specs" "^0.3.0" "@sigstore/protobuf-specs" "^0.3.0"
"@tsconfig/node20@^20.1.2": "@tsconfig/node20@^20.1.4":
version "20.1.2" version "20.1.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.2.tgz#b93128c411d38e9507035255195bc8a6718541e3" resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
integrity sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ== integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
"@tufjs/canonical-json@2.0.0": "@tufjs/canonical-json@2.0.0":
version "2.0.0" version "2.0.0"
@@ -805,10 +805,10 @@
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23" resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg== integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
"@types/node@^20.11.25": "@types/node@^20.12.2":
version "20.11.25" version "20.12.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.25.tgz#0f50d62f274e54dd7a49f7704cc16bfbcccaf49f" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
integrity sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw== integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
@@ -851,26 +851,26 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37"
integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==
"@volar/language-core@2.1.2", "@volar/language-core@~2.1.2": "@volar/language-core@2.1.5", "@volar/language-core@~2.1.3":
version "2.1.2" version "2.1.5"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.2.tgz#2053c0ee48a822d5418be2c192e51e580764b49f" resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.5.tgz#78803531562ca423a3b4fa1b3a90d33537aab928"
integrity sha512-5qsDp0Gf6fE09UWCeK7bkVn6NxMwC9OqFWQkMMkeej8h8XjyABPdRygC2RCrqDrfVdGijqlMQeXs6yRS+vfZYA== integrity sha512-u1OHmVkCFsJqNdaM2GKuMhE67TxcEnOqJNF+VtYv2Ji8DnrUaF4FAFSNxY+MRGICl+873CsSJVKas9TQtW14LA==
dependencies: dependencies:
"@volar/source-map" "2.1.2" "@volar/source-map" "2.1.5"
"@volar/source-map@2.1.2": "@volar/source-map@2.1.5":
version "2.1.2" version "2.1.5"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.2.tgz#d270ff8ef5c814582f0efe08272c0fd6b9effb3b" resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.5.tgz#c073549368d399365f0024ed89aea93de0807328"
integrity sha512-yFJqsuLm1OaWrsz9E3yd3bJcYIlHqdZ8MbmIoZLrAzMYQDcoF26/INIhgziEXSdyHc8xd7rd/tJdSnUyh0gH4Q== integrity sha512-GIkAM6fHgDcTXcdH4i10fAiAZzO0HLIer8/pt3oZ9A0n7n4R5d1b2F8Xxzh/pgmgNoL+SrHX3MFxs35CKgfmtA==
dependencies: dependencies:
muggle-string "^0.4.0" muggle-string "^0.4.0"
"@volar/typescript@~2.1.2": "@volar/typescript@~2.1.3":
version "2.1.2" version "2.1.5"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.2.tgz#61f838cf4410e328a7ba638fadc41bb814772508" resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.5.tgz#756a8626af1c5ce3c9d8df3e79d49bfdd0adf39e"
integrity sha512-lhTancZqamvaLvoz0u/uth8dpudENNt2LFZOWCw9JZiX14xRFhdhfzmphiCRb7am9E6qAJSbdS/gMt1utXAoHQ== integrity sha512-zo9a3NrNMSkufIvHuExDGTfYv+zO7C5p2wg8fyP7vcqF/Qo0ztjb0ZfOgq/A85EO/MBc1Kj2Iu7PaOBtP++NMw==
dependencies: dependencies:
"@volar/language-core" "2.1.2" "@volar/language-core" "2.1.5"
path-browserify "^1.0.1" path-browserify "^1.0.1"
"@vue/babel-helper-vue-transform-on@1.2.1": "@vue/babel-helper-vue-transform-on@1.2.1":
@@ -948,12 +948,12 @@
"@vue/compiler-dom" "3.4.21" "@vue/compiler-dom" "3.4.21"
"@vue/shared" "3.4.21" "@vue/shared" "3.4.21"
"@vue/language-core@2.0.6": "@vue/language-core@2.0.7":
version "2.0.6" version "2.0.7"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.6.tgz#876f90622a3f801dce5cedcd6eae429d732152e2" resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.7.tgz#af12f752a93c4d2498626fca33f5d1ddc8c5ceb9"
integrity sha512-UzqU12tzf9XLqRO3TiWPwRNpP4fyUzE6MAfOQWQNZ4jy6a30ARRUpmODDKq6O8C4goMc2AlPqTmjOHPjHkilSg== integrity sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==
dependencies: dependencies:
"@volar/language-core" "~2.1.2" "@volar/language-core" "~2.1.3"
"@vue/compiler-dom" "^3.4.0" "@vue/compiler-dom" "^3.4.0"
"@vue/shared" "^3.4.0" "@vue/shared" "^3.4.0"
computeds "^0.0.1" computeds "^0.0.1"
@@ -1481,34 +1481,34 @@ error-ex@^1.3.1:
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
esbuild@^0.19.3: esbuild@^0.20.1:
version "0.19.12" version "0.20.2"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
optionalDependencies: optionalDependencies:
"@esbuild/aix-ppc64" "0.19.12" "@esbuild/aix-ppc64" "0.20.2"
"@esbuild/android-arm" "0.19.12" "@esbuild/android-arm" "0.20.2"
"@esbuild/android-arm64" "0.19.12" "@esbuild/android-arm64" "0.20.2"
"@esbuild/android-x64" "0.19.12" "@esbuild/android-x64" "0.20.2"
"@esbuild/darwin-arm64" "0.19.12" "@esbuild/darwin-arm64" "0.20.2"
"@esbuild/darwin-x64" "0.19.12" "@esbuild/darwin-x64" "0.20.2"
"@esbuild/freebsd-arm64" "0.19.12" "@esbuild/freebsd-arm64" "0.20.2"
"@esbuild/freebsd-x64" "0.19.12" "@esbuild/freebsd-x64" "0.20.2"
"@esbuild/linux-arm" "0.19.12" "@esbuild/linux-arm" "0.20.2"
"@esbuild/linux-arm64" "0.19.12" "@esbuild/linux-arm64" "0.20.2"
"@esbuild/linux-ia32" "0.19.12" "@esbuild/linux-ia32" "0.20.2"
"@esbuild/linux-loong64" "0.19.12" "@esbuild/linux-loong64" "0.20.2"
"@esbuild/linux-mips64el" "0.19.12" "@esbuild/linux-mips64el" "0.20.2"
"@esbuild/linux-ppc64" "0.19.12" "@esbuild/linux-ppc64" "0.20.2"
"@esbuild/linux-riscv64" "0.19.12" "@esbuild/linux-riscv64" "0.20.2"
"@esbuild/linux-s390x" "0.19.12" "@esbuild/linux-s390x" "0.20.2"
"@esbuild/linux-x64" "0.19.12" "@esbuild/linux-x64" "0.20.2"
"@esbuild/netbsd-x64" "0.19.12" "@esbuild/netbsd-x64" "0.20.2"
"@esbuild/openbsd-x64" "0.19.12" "@esbuild/openbsd-x64" "0.20.2"
"@esbuild/sunos-x64" "0.19.12" "@esbuild/sunos-x64" "0.20.2"
"@esbuild/win32-arm64" "0.19.12" "@esbuild/win32-arm64" "0.20.2"
"@esbuild/win32-ia32" "0.19.12" "@esbuild/win32-ia32" "0.20.2"
"@esbuild/win32-x64" "0.19.12" "@esbuild/win32-x64" "0.20.2"
escalade@^3.1.1: escalade@^3.1.1:
version "3.1.2" version "3.1.2"
@@ -2410,14 +2410,14 @@ postcss-selector-parser@^6.0.10:
cssesc "^3.0.0" cssesc "^3.0.0"
util-deprecate "^1.0.2" util-deprecate "^1.0.2"
postcss@^8.4.35: postcss@^8.4.35, postcss@^8.4.38:
version "8.4.35" version "8.4.38"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
dependencies: dependencies:
nanoid "^3.3.7" nanoid "^3.3.7"
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.2.0"
prebuild-install@^7.1.1: prebuild-install@^7.1.1:
version "7.1.2" version "7.1.2"
@@ -2551,26 +2551,26 @@ retry@^0.12.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
rollup@^4.2.0: rollup@^4.13.0:
version "4.12.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a"
integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q== integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==
dependencies: dependencies:
"@types/estree" "1.0.5" "@types/estree" "1.0.5"
optionalDependencies: optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.12.0" "@rollup/rollup-android-arm-eabi" "4.13.0"
"@rollup/rollup-android-arm64" "4.12.0" "@rollup/rollup-android-arm64" "4.13.0"
"@rollup/rollup-darwin-arm64" "4.12.0" "@rollup/rollup-darwin-arm64" "4.13.0"
"@rollup/rollup-darwin-x64" "4.12.0" "@rollup/rollup-darwin-x64" "4.13.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.12.0" "@rollup/rollup-linux-arm-gnueabihf" "4.13.0"
"@rollup/rollup-linux-arm64-gnu" "4.12.0" "@rollup/rollup-linux-arm64-gnu" "4.13.0"
"@rollup/rollup-linux-arm64-musl" "4.12.0" "@rollup/rollup-linux-arm64-musl" "4.13.0"
"@rollup/rollup-linux-riscv64-gnu" "4.12.0" "@rollup/rollup-linux-riscv64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-gnu" "4.12.0" "@rollup/rollup-linux-x64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-musl" "4.12.0" "@rollup/rollup-linux-x64-musl" "4.13.0"
"@rollup/rollup-win32-arm64-msvc" "4.12.0" "@rollup/rollup-win32-arm64-msvc" "4.13.0"
"@rollup/rollup-win32-ia32-msvc" "4.12.0" "@rollup/rollup-win32-ia32-msvc" "4.13.0"
"@rollup/rollup-win32-x64-msvc" "4.12.0" "@rollup/rollup-win32-x64-msvc" "4.13.0"
fsevents "~2.3.2" fsevents "~2.3.2"
safe-buffer@^5.0.1, safe-buffer@~5.2.0: 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" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 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: source-map-support@~0.5.20:
version "0.5.21" version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 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" mkdirp "^1.0.3"
yallist "^4.0.0" yallist "^4.0.0"
terser@^5.29.1: terser@^5.30.0:
version "5.29.1" version "5.30.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.1.tgz#44e58045b70c09792ba14bfb7b4e14ca8755b9fa" resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
integrity sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ== integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
dependencies: dependencies:
"@jridgewell/source-map" "^0.3.3" "@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2" acorn "^8.8.2"
commander "^2.20.0" commander "^2.20.0"
source-map-support "~0.5.20" 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": "three-orientation-gizmo@https://github.com/jrj2211/three-orientation-gizmo":
version "1.1.0" version "1.1.0"
resolved "https://github.com/jrj2211/three-orientation-gizmo#000281f0559c316f72cdd23a1885d63ae6901095" resolved "https://github.com/jrj2211/three-orientation-gizmo#000281f0559c316f72cdd23a1885d63ae6901095"
@@ -2935,10 +2945,10 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
typescript@~5.4.2: typescript@~5.4.3:
version "5.4.2" version "5.4.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff"
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
undici-types@~5.26.4: undici-types@~5.26.4:
version "5.26.5" version "5.26.5"
@@ -2992,14 +3002,14 @@ validate-npm-package-name@^5.0.0:
dependencies: dependencies:
builtins "^5.0.0" builtins "^5.0.0"
vite@^5.1.5: vite@^5.2.7:
version "5.1.5" version "5.2.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.5.tgz#bdbc2b15e8000d9cc5172f059201178f9c9de5fb" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
integrity sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q== integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
dependencies: dependencies:
esbuild "^0.19.3" esbuild "^0.20.1"
postcss "^8.4.35" postcss "^8.4.38"
rollup "^4.2.0" rollup "^4.13.0"
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
@@ -3011,13 +3021,13 @@ vue-template-compiler@^2.7.14:
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.2.0" he "^1.2.0"
vue-tsc@^2.0.6: vue-tsc@^2.0.7:
version "2.0.6" version "2.0.7"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.6.tgz#29cb195ffea63d66ec70b64eb4aadc6cd162bb12" resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.7.tgz#3177a2fe720bfa7355d3717929ee8c8d132bc5d0"
integrity sha512-kK50W4XqQL34vHRkxlRWLicrT6+F9xfgCgJ4KSmCHcytKzc1u3c94XXgI+CjmhOSxyw0krpExF7Obo7y4+0dVQ== integrity sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==
dependencies: dependencies:
"@volar/typescript" "~2.1.2" "@volar/typescript" "~2.1.3"
"@vue/language-core" "2.0.6" "@vue/language-core" "2.0.7"
semver "^7.5.4" semver "^7.5.4"
vue@^3.4.21: vue@^3.4.21:
@@ -3031,10 +3041,10 @@ vue@^3.4.21:
"@vue/server-renderer" "3.4.21" "@vue/server-renderer" "3.4.21"
"@vue/shared" "3.4.21" "@vue/shared" "3.4.21"
vuetify@^3.5.8: vuetify@^3.5.13:
version "3.5.8" version "3.5.13"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.8.tgz#bc8f08dfd3314640e7b5d43b50138a26d650cbbf" resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
integrity sha512-8nGS+lKejZkev55HFwIfsRt+9fOqbeDQNmXxfmLKAlnUT8FtynVwbjAwHMtX/OQAQ3ZwRaR1ptqQQmx3OgxzbQ== integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
walk-up-path@^3.0.1: walk-up-path@^3.0.1:
version "3.0.1" version "3.0.1"