Compare commits

...

87 Commits

Author SHA1 Message Date
Yeicor
1f30c2fd0a Automatically update version to 0.8.9 2024-04-03 18:05:59 +00:00
Yeicor
ba05a8072b Minor frontend fix for long object names 2024-04-03 20:01:46 +02:00
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
Yeicor
9c2656d7db Automatically update version to 0.6.13 2024-03-10 17:57:32 +00:00
Yeicor
161d76ee69 Merge remote-tracking branch 'origin/master' 2024-03-10 18:56:52 +01:00
Yeicor
431c41a615 fix CI deployment 12 2024-03-10 18:56:45 +01:00
Yeicor
7144eb39da Automatically update version to 0.6.12 2024-03-10 17:41:13 +00:00
Yeicor
8e1c89ad6d fix CI deployment 11 2024-03-10 18:40:13 +01:00
Yeicor
7f692c0b52 Automatically update version to 0.6.11 2024-03-10 17:32:32 +00:00
Yeicor
86043132a8 Merge remote-tracking branch 'origin/master' 2024-03-10 18:32:00 +01:00
Yeicor
23b4d25464 fix CI deployment 10 2024-03-10 18:31:52 +01:00
Yeicor
22514d8603 Automatically update version to 0.6.10 2024-03-10 17:27:00 +00:00
Yeicor
b440a89b13 Merge remote-tracking branch 'origin/master' 2024-03-10 18:26:21 +01:00
Yeicor
cbdb5aff5e fix CI deployment 10 2024-03-10 18:26:13 +01:00
Yeicor
a3a9258a78 Automatically update version to 0.6.9 2024-03-10 17:21:51 +00:00
Yeicor
9f30ac8eb7 Merge remote-tracking branch 'origin/master' 2024-03-10 18:21:08 +01:00
Yeicor
e11c9dd5c6 fix CI deployment 9 2024-03-10 18:21:00 +01:00
Yeicor
520b89af4a Automatically update version to 0.6.8 2024-03-10 17:17:37 +00:00
40 changed files with 1897 additions and 1098 deletions

View File

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

View File

@@ -19,6 +19,8 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
with:
ref: "${{ inputs.ref }}"
- uses: "actions/setup-node@v4"
with:
cache: "yarn"
@@ -35,6 +37,8 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
@@ -48,6 +52,8 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
@@ -66,13 +72,15 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
cache: "poetry"
- run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "PYTHONPATH=yacv_server YACV_DISABLE_SERVER=true poetry run python example/object.py"
- run: "YACV_DISABLE_SERVER=true poetry run python example/object.py"
- uses: "actions/upload-artifact@v4"
with:
name: "example"

View File

@@ -43,7 +43,7 @@ jobs:
if git commit -am "Automatically update version to $CLEAN_VERSION"; then
git push
# Move the tag to the new commit
git tag -f -a "$CLEAN_VERSION" -m "$CLEAN_VERSION"
git tag -f -a "v$CLEAN_VERSION" -m "v$CLEAN_VERSION"
git push -f --tags # Force push the tag to GitHub
# The tag move will NOT trigger a new workflow
else

View File

@@ -32,25 +32,15 @@ jobs:
name: "github-pages"
url: "${{ steps.deployment.outputs.page_url }}"
steps:
- uses: "dawidd6/action-download-artifact@v3"
with:
workflow: "build.yml"
name: "frontend"
- uses: "actions/download-artifact@v4"
with: # Downloads all artifacts from the build job
path: "./public"
allow_forks: false
- uses: "dawidd6/action-download-artifact@v3"
with:
workflow: "build.yml"
name: "logo"
path: "./public"
allow_forks: false
- uses: "dawidd6/action-download-artifact@v3"
with:
workflow: "build.yml"
name: "example"
path: "./public"
allow_forks: false
- uses: "actions/configure-pages@v4"
- run: | # Merge the subdirectories of public into a single directory
for dir in public/*; do
mv "$dir/"* public/
rmdir "$dir"
done
- uses: "actions/configure-pages@v5"
- uses: "actions/upload-pages-artifact@v3"
with:
path: 'public'
@@ -71,7 +61,15 @@ jobs:
- uses: "actions/checkout@v4"
with:
ref: "${{ inputs.ref }}"
- uses: "JRubics/poetry-publish@v2.0"
- uses: "actions/setup-node@v4"
with:
cache: "yarn"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
pypi_token: "${{ secrets.PYPI_TOKEN }}"
cache: "poetry"
- run: "poetry install"
- run: "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}"
- run: "poetry publish --build"

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 [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
- Load multiple models at once, load external models and even images as quads.
- View and interact with topological entities: faces, edges, vertices and locations.
- Control clipping planes and transparency of each model.
- View and interact with topological entities: faces, edges, vertices and locations.
- Select any entity and measure bounding box size and distances.
- Fully-featured static deployment: just upload the viewer and models to your server.
- Hot reloading while editing the CAD model (using the `yacv-server` package).
- Fully-featured static deployment: just upload the viewer and models to your server.
## Usage
The [example](example) is a fully working project that shows how to use the viewer.
You can play with the latest
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
(or
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
![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.2.0
This package contains the following license and notice below:
These packages each contain the following license and notice below:
Copyright (c) 2009-2011, Mozilla Foundation and contributors
All rights reserved.
@@ -1290,7 +1291,7 @@ third-party archives.
The following npm package may be included in this product:
- typescript@5.4.2
- typescript@5.4.3
This package contains the following license and notice below:
@@ -1764,6 +1765,36 @@ SOFTWARE.
-----------
The following npm package may be included in this product:
- three-mesh-bvh@0.7.3
This package contains the following license and notice below:
MIT License
Copyright (c) 2018 Garrett Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- napi-build-utils@1.0.2
@@ -2408,7 +2439,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- vuetify@3.5.8
- vuetify@3.5.13
This package contains the following license and notice below:
@@ -2690,9 +2721,9 @@ THE SOFTWARE.
The following npm packages may be included in this product:
- @gltf-transform/core@3.10.0
- @gltf-transform/extensions@3.10.0
- @gltf-transform/functions@3.10.0
- @gltf-transform/core@3.10.1
- @gltf-transform/extensions@3.10.1
- @gltf-transform/functions@3.10.1
These packages each contain the following license and notice below:
@@ -2843,7 +2874,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- postcss@8.4.35
- postcss@8.4.38
This package contains the following license and notice below:

View File

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

View File

@@ -1,13 +1,13 @@
<!--suppress SillyAssignmentJS -->
<script setup lang="ts">
import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef} from "vue";
<script lang="ts" setup>
import {defineAsyncComponent, provide, type Ref, ref, shallowRef, triggerRef, watch} from "vue";
import Sidebar from "./misc/Sidebar.vue";
import Loading from "./misc/Loading.vue";
import Tools from "./tools/Tools.vue";
import Models from "./models/Models.vue";
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
import {settings} from "./misc/settings";
import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
import {SceneMgr} from "./misc/scene";
import {Document} from "@gltf-transform/core";
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
@@ -28,39 +28,67 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
const sceneDocument = shallowRef(new Document());
provide('sceneDocument', {sceneDocument});
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
const disableTap = ref(false);
const setDisableTap = (val: boolean) => disableTap.value = val;
provide('disableTap', {disableTap, setDisableTap});
async function onModelUpdateRequest(event: NetworkUpdateEvent) {
// Trigger progress bar as soon as possible (also triggered earlier for each raw notification)
if (viewer.value && event.models.length > 0) {
viewer.value.onProgress(0.10);
}
// Load/unload a new batch of models to optimize rendering time
console.log("Received model update request", event.models);
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null);
let shutdownRequest = null;
if (shutdownRequestIndex !== -1) {
console.log("Will shut down the connection after this load, as requested by the server");
shutdownRequest = event.models.splice(shutdownRequestIndex, 1)[0];
}
let doc = sceneDocument.value;
for (let modelIndex in event.models) {
let isLast = parseInt(modelIndex) === event.models.length - 1;
let model = event.models[modelIndex];
if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast, isLast);
} else {
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast);
tools.value?.removeObjectSelections(model.name);
try {
if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
} else {
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
}
} catch (e) {
console.error("Error loading model", model, e);
}
}
if (shutdownRequest !== null) {
console.log("Shutting down the connection as requested by the server");
event.disconnect();
}
sceneDocument.value = doc
triggerRef(sceneDocument); // Why not triggered automatically?
}
async function onModelRemoveRequest(name: string) {
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
triggerRef(sceneDocument); // Why not triggered automatically?
await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
}));
}
// Set up the load model event listener
let networkMgr = new NetworkManager();
networkMgr.addEventListener('update-early',
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
// Start loading all configured models ASAP
for (let model of settings.preload) {
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() {
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>
<!-- 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>
<v-toolbar-title>Models</v-toolbar-title>
</template>
<template #toolbar-items>
<v-btn icon="" @click="loadModelManual">
<svg-icon type="mdi" :path="mdiPlus"/>
<svg-icon :path="mdiPlus" type="mdi"/>
</v-btn>
</template>
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
</sidebar>
<!-- 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>
<v-toolbar-title>Tools</v-toolbar-title>
</template>
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
</sidebar>
</v-layout>

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
import type {MObject3D} from "../tools/Selection.vue";
import type { ModelScene } from '@google/model-viewer/lib/three-components/ModelScene';
import type {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene';
import type {SelectionInfo} from "../tools/selection";
function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
center: Vector3,
vertices: Array<Vector3>
} {
obj.updateMatrixWorld();
let pos: BufferAttribute | InterleavedBufferAttribute = obj.geometry.getAttribute('position');
let ind: BufferAttribute | null = obj.geometry.index;
if (!ind) {
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
let ind: BufferAttribute | null = selInfo.object.geometry.index;
if (ind === null) {
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
for (let i = 0; i < pos.count; i++) {
ind.array[i] = i;
@@ -18,14 +18,14 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
}
let center = new Vector3();
let vertices = [];
for (let i = 0; i < ind.count; i++) {
let index = ind.array[i];
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let index = ind.getX(i)
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
vertex = scene.target.worldToLocal(obj.localToWorld(vertex));
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
center.add(vertex);
vertices.push(vertex);
}
center = center.divideScalar(ind.count);
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
return {center, vertices};
}
@@ -33,7 +33,7 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): {
* Given two THREE.Object3D objects, returns their closest and farthest vertices, and the geometric centers.
* All of them are approximated and should not be used for precise calculations.
*/
export function distances(a: MObject3D, b: MObject3D, scene: ModelScene): {
export function distances(a: SelectionInfo, b: SelectionInfo, scene: ModelScene): {
min: Array<Vector3>,
center: Array<Vector3>,
max: Array<Vector3>

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";
let io = new WebIO();
@@ -12,9 +12,41 @@ export let extrasNameValueHelpers = "__helpers";
*
* Remember to call mergeFinalize after all models have been merged (slower required operations).
*/
export async function mergePartial(url: string, name: string, document: Document): Promise<Document> {
export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => {
}): Promise<Document> {
// Fetch the complete document from the network
// This could be done at the same time as the document is being processed, but I wanted better metrics
let response = await fetch(url);
let buffer = await response.arrayBuffer();
networkFinished();
// Load the new document
let newDoc = await io.read(url);
let newDoc = null;
let alreadyTried: { [name: string]: boolean } = {}
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
try { // Try to load fast if no extensions are used
newDoc = await io.readBinary(new Uint8Array(buffer));
} catch (e) { // Fallback to wait for download and register big extensions
if (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
io.registerExtensions([KHRDracoMeshCompression])
.registerDependencies({
'draco3d.decoder': await dracoDecoderWeb.default({}),
'draco3d.encoder': await dracoEncoderWeb.default({})
});
} else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
io.registerExtensions([EXTTextureWebP]);
} else { // TODO: Add more extensions as required
throw e;
}
}
}
// Remove any previous model with the same name
await document.transform(dropByName(name));

View File

@@ -1,3 +1,5 @@
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
import {Document, type TypedArray} from '@gltf-transform/core'
import {Vector2} from "three/src/math/Vector2.js"
import {Vector3} from "three/src/math/Vector3.js"
@@ -26,7 +28,7 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
if (rawColors) {
colors = doc.createAccessor(name + 'Color')
.setArray(new Float32Array(rawColors) as TypedArray)
.setType('VEC3')
.setType('VEC4')
.setBuffer(buffer);
}
const material = doc.createMaterial(name + 'Material')
@@ -39,6 +41,11 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
if (rawColors) {
geometry.setAttribute('COLOR_0', colors)
}
if (mode == WebGL2RenderingContext.TRIANGLES) {
geometry.setExtras({face_triangles_end: [rawIndices.length / 6, rawIndices.length * 2 / 6, rawIndices.length * 3 / 6, rawIndices.length * 4 / 6, rawIndices.length * 5 / 6, rawIndices.length]})
} else if (mode == WebGL2RenderingContext.LINES) {
geometry.setExtras({edge_points_end: [rawIndices.length / 3, rawIndices.length * 2 / 3, rawIndices.length]})
}
const mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
scene.addChild(node)
@@ -48,21 +55,25 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
*/
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
let rawIndices = [0, 1, 2, 3, 4, 5];
let rawPositions = [
[0, 0, 0, size.x, 0, 0],
[0, 0, 0, 0, size.y, 0],
[0, 0, 0, 0, 0, -size.z],
0, 0, 0, size.x, 0, 0,
0, 0, 0, 0, size.y, 0,
0, 0, 0, 0, 0, -size.z,
];
let rawIndices = [0, 1];
let rawColors = [
[...(AxesColors.x[0]), ...(AxesColors.x[1])],
[...(AxesColors.y[0]), ...(AxesColors.y[1])],
[...(AxesColors.z[0]), ...(AxesColors.z[1])],
].map(g => g.map(x => x / 255.0));
buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes');
buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes');
buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes');
buildSimpleGltf(doc, [0, 0, 0], [0], null, transform, '__helper_axes', WebGL2RenderingContext.POINTS);
...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
].map(x => x / 255.0);
// Axes at (0, 0, 0)
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
// Axes at center
if (new Matrix4() != transform) {
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes_center');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes_center', WebGL2RenderingContext.POINTS);
}
}
/**
@@ -71,8 +82,10 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
* The grid is built as a box of triangles (representing lines) looking to the inside of the box.
* This ensures that only the back of the grid is always visible, regardless of the camera position.
*/
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4 = new Matrix4(), divisions = 10) {
export async function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
// Create transformed positions for the inner faces of the box
let allPositions: number[] = [];
let allIndices: number[] = [];
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
for (let positive of [1, -1]) {
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
@@ -82,13 +95,25 @@ export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4
if (axis.x) size2.set(size.z, size.y);
if (axis.y) size2.set(size.x, size.z);
if (axis.z) size2.set(size.x, size.y);
let transform = baseTransform.clone().multiply(translation).multiply(rotation);
newGridPlane(doc, size2, transform, divisions);
let transform = new Matrix4().multiply(translation).multiply(rotation);
let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
let baseIndex = allPositions.length / 3;
for (let i of rawIndices) {
allIndices.push(i + baseIndex);
}
// Apply transform to the positions before adding them to the list
for (let i = 0; i < rawPositions.length; i += 3) {
let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
pos.applyMatrix4(transform);
allPositions.push(pos.x, pos.y, pos.z);
}
}
}
let colors = new Array(allPositions.length / 3 * 4).fill(1);
buildSimpleGltf(doc, allPositions, allIndices, colors, baseTransform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
}
export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.002) {
export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] {
const rawPositions = [];
const rawIndices = [];
// Build the grid as triangles
@@ -114,5 +139,5 @@ export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 =
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
}
buildSimpleGltf(doc, rawPositions, rawIndices, null, transform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
return [rawPositions, rawIndices];
}

55
frontend/misc/lines.ts Normal file
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
class NetworkUpdateEventModel {
export class NetworkUpdateEventModel {
name: string;
url: string;
// TODO: Detect and manage instances of the same object (same hash, different name)
hash: string | null;
isRemove: boolean;
isRemove: boolean | null; // This is null for a shutdown event
constructor(name: string, url: string, hash: string | null, isDelete: boolean) {
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) {
this.name = name;
this.url = url;
this.hash = hash;
this.isRemove = isDelete;
this.isRemove = isRemove;
}
}
export class NetworkUpdateEvent extends Event {
models: NetworkUpdateEventModel[];
disconnect: () => void;
constructor(models: NetworkUpdateEventModel[]) {
constructor(models: NetworkUpdateEventModel[], disconnect: () => void) {
super("update");
this.models = models;
this.disconnect = disconnect;
}
}
@@ -57,48 +59,81 @@ export class NetworkManager extends EventTarget {
}
}
private async monitorDevServer(url: URL) {
try {
// WARNING: This will spam the console logs with failed requests when the server is down
let response = await fetch(url.toString());
// console.log("Monitoring", url.toString(), response);
if (response.status === 200) {
let lines = readLinesStreamings(response.body!.getReader());
for await (let line of lines) {
if (!line || !line.startsWith("data:")) continue;
let data = JSON.parse(line.slice(5));
// console.debug("WebSocket message", data);
let urlObj = new URL(url);
urlObj.searchParams.delete("api_updates");
urlObj.searchParams.set("api_object", data.name);
this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove);
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
while (!stop()) {
try {
// WARNING: This will spam the console logs with failed requests when the server is down
const controller = new AbortController();
let response = await fetch(url.toString(), {signal: controller.signal});
// console.log("Monitoring", url.toString(), response);
if (response.status === 200) {
let lines = readLinesStreamings(response.body!.getReader());
for await (let line of lines) {
if (stop()) break;
if (!line || !line.startsWith("data:")) continue;
let data: { name: string, hash: string, is_remove: boolean | null } = JSON.parse(line.slice(5));
// console.debug("WebSocket message", data);
let urlObj = new URL(url);
urlObj.searchParams.delete("api_updates");
urlObj.searchParams.set("api_object", data.name);
this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove, async () => {
controller.abort(); // Notify the server that we are done
});
}
} else {
// Server is down, wait a little longer before retrying
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
}
controller.abort();
} catch (e) { // Ignore errors (retry very soon)
}
} catch (e) { // Ignore errors (retry very soon)
await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
}
setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs);
return;
}
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean) {
let prevHash = this.knownObjectHashes[name];
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash);
if (!hash || hash !== prevHash || isRemove) {
if (!isRemove) {
this.knownObjectHashes[name] = hash;
} else {
delete this.knownObjectHashes[name];
}
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove);
this.bufferedUpdates.push(newModel);
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
}) {
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
// Optimization: try to batch updates automatically for faster rendering
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
this.batchTimeout = setTimeout(() => {
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates));
this.bufferedUpdates = [];
}, batchTimeout);
}
// We only care about the latest update per model name
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
// Add the new model to the list of updates and dispatch the early update
let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
this.bufferedUpdates.push(upd);
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
// Optimization: try to batch updates automatically for faster rendering
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
this.batchTimeout = setTimeout(() => {
// Update known hashes for minimal updates
for (let model of this.bufferedUpdates) {
if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) {
// Delete this useless update
let foundFirst = false;
this.bufferedUpdates = this.bufferedUpdates.filter(m => {
if (m === model) {
if (!foundFirst) { // Remove only first full match
foundFirst = true;
return false;
}
}
return true;
})
} else {
// Keep this update and update the last known hash
if (model.isRemove == true) {
if (model.name in this.knownObjectHashes) delete this.knownObjectHashes[model.name];
} else if (model.isRemove == false) {
this.knownObjectHashes[model.name] = model.hash;
}
}
}
// Dispatch the event to actually update the models
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
this.bufferedUpdates = [];
}, batchTimeout);
}
}

View File

@@ -11,41 +11,32 @@ export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
let loadStart = performance.now();
let loadNetworkEnd: number;
// Start merging into the current document, replacing or adding as needed
document = await mergePartial(url, name, document);
try {
// Start merging into the current document, replacing or adding as needed
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
} finally {
if (updateHelpers) {
// Reload the helpers to fit the new model
await this.reloadHelpers(sceneUrl, document, reloadScene);
reloadScene = false;
}
if (updateHelpers) {
// Reload the helpers to fit the new model
await this.reloadHelpers(sceneUrl, document, reloadScene);
reloadScene = false;
}
if (reloadScene) {
// Display the final fully loaded model
let displayStart = performance.now();
document = await this.showCurrentDoc(sceneUrl, document);
console.log("Scene displayed in", performance.now() - displayStart, "ms");
if (reloadScene) {
// Display the final fully loaded model
let displayStart = performance.now();
document = await this.showCurrentDoc(sceneUrl, document);
console.log("Scene displayed in", performance.now() - displayStart, "ms");
}
}
return document;
}
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);
}
static getBoundingBox(document: Document): Box3 | null {
if (document.getRoot().listNodes().length === 0) return null;
// Get bounding box of the model and use it to set the size of the helpers
@@ -87,6 +78,19 @@ export class SceneMgr {
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 */
private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
// 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
let buffer = await toBuffer(document);
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
console.debug("Showing current doc", document, "as", Array.from(buffer));
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
sceneUrl.value = URL.createObjectURL(blob);
return document;

View File

@@ -12,12 +12,17 @@ export const settings = {
// Websocket URLs automatically listen for new models from the python backend
"dev+http://127.0.0.1:32323/"
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,
// ModelViewer settings
autoplay: true,
autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1,
shadowIntensity: 0,
background: '',

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import {
VBtn,
VBtnToggle,
@@ -12,8 +12,8 @@ import {
VTooltip,
} from "vuetify/lib/components/index.mjs";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
import {Document, Mesh} from "@gltf-transform/core";
import {inject, ref, type ShallowRef, watch} from "vue";
import {Mesh} from "@gltf-transform/core";
import {ref, watch} from "vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {
mdiCircleOpacity,
@@ -22,16 +22,18 @@ import {
mdiRectangle,
mdiRectangleOutline,
mdiSwapHorizontal,
mdiVectorLine,
mdiVectorRectangle
} from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon';
import {SceneMgr} from "../misc/scene";
import {BackSide, FrontSide} from "three/src/constants.js";
import {Box3} from "three/src/math/Box3.js";
import {Color} from "three/src/math/Color.js";
import {Plane} from "three/src/math/Plane.js";
import {Vector3} from "three/src/math/Vector3.js";
import type {MObject3D} from "../tools/Selection.vue";
import {toLineSegments} from "../misc/lines.js";
import {settings} from "../misc/settings.js"
const props = defineProps<{
meshes: Array<Mesh>,
@@ -41,25 +43,24 @@ const emit = defineEmits<{ remove: [] }>()
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
// Reactive properties
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
const opacity = defineModel<number>("opacity", {default: 1});
// Count the number of faces, edges and vertices
let faceCount = ref(-1);
let edgeCount = ref(-1);
let vertexCount = ref(-1);
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
const clipPlaneX = ref(1);
const clipPlaneSwappedX = ref(false);
const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(settings.edgeWidth);
// Count the number of faces, edges and vertices
let faceCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES).length).reduce((a, b) => a + b, 0)
let edgeCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]).length).reduce((a, b) => a + b, 0)
let vertexCount = props.meshes.map((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS).length).reduce((a, b) => a + b, 0)
// Set initial defaults for the enabled features
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
// Misc properties
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
const opacity = defineModel<number>("opacity", {default: 1});
const wireframe = ref(false);
// Listeners for changes in the properties (or viewer reloads)
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
@@ -67,13 +68,10 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
// Iterate all primitives of the mesh and set their visibility based on the enabled features
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
// of not actually removing the primitives from the scene graph
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
let childIsEdge = child.type == 'Line' || child.type == 'LineSegments'
let childIsEdge = child.type == 'Line' || child.type == 'LineSegments' || child.type == 'LineSegments2'
let childIsVertex = child.type == 'Points'
if (childIsFace || childIsEdge || childIsVertex) {
let visible = newEnabledFeatures.includes(childIsFace ? 0 : childIsEdge ? 1 : childIsVertex ? 2 : -1);
@@ -87,16 +85,12 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
scene.queueRender()
}
watch(enabledFeatures, onEnabledFeaturesChange);
watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
function onOpacityChange(newOpacity: number) {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
// Iterate all primitives of the mesh and set their opacity based on the enabled features
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
// of not actually removing the primitives from the scene graph
// console.log('Opacity may have changed', newOpacity)
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.material && child.material.opacity !== newOpacity) {
@@ -111,7 +105,22 @@ function onOpacityChange(newOpacity: number) {
watch(opacity, onOpacityChange);
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
function onWireframeChange(newWireframe: boolean) {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.material && child.material.wireframe !== newWireframe) {
child.material.wireframe = newWireframe;
child.material.needsUpdate = true;
}
}
});
scene.queueRender()
}
watch(wireframe, onWireframeChange);
function onClipPlanesChange() {
let scene = props.viewer?.scene;
@@ -125,22 +134,25 @@ function onClipPlanesChange() {
if (props.viewer?.renderer && (enabledX || enabledY || enabledZ)) {
// Global value for all models, once set it cannot be unset (unknown for other models...)
props.viewer.renderer.threeRenderer.localClippingEnabled = true;
// Due to model-viewer's camera manipulation, the bounding box needs to be transformed
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
if (!boundingBox) return; // No models. Should not happen.
bbox = boundingBox.translate(scene.getTarget());
// Get the bounding box containing all features of this model
bbox = new Box3();
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
bbox.expandByObject(child);
}
});
}
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.material) {
if (bbox) {
if (bbox?.isEmpty() == false) {
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x);
let offsetY = bbox.min.z + clipPlaneY.value * (bbox.max.z - bbox.min.z);
let offsetZ = bbox.min.y + clipPlaneZ.value * (bbox.max.y - bbox.min.y);
let offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y);
let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z);
let planes = [
new Plane(new Vector3(-1, 0, 0), offsetX),
new Plane(new Vector3(0, 0, 1), offsetY),
new Plane(new Vector3(0, -1, 0), offsetZ),
new Plane(new Vector3(0, -1, 0), offsetY),
new Plane(new Vector3(0, 0, 1), -offsetZ),
];
if (clipPlaneSwappedX.value) planes[0].negate();
if (clipPlaneSwappedY.value) planes[1].negate();
@@ -166,20 +178,106 @@ watch(clipPlaneZ, onClipPlanesChange);
watch(clipPlaneSwappedX, onClipPlanesChange);
watch(clipPlaneSwappedY, 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() {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
// Iterate all primitives of the mesh and set their visibility based on the enabled features
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
// of not actually removing the primitives from the scene graph
// Count the number of faces, edges and vertices
const isFirstLoad = faceCount.value === -1;
faceCount.value = props.meshes
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
.reduce((a, b) => a + b, 0)
edgeCount.value = props.meshes
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
.reduce((a, b) => a + b, 0)
vertexCount.value = props.meshes
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
.reduce((a, b) => a + b, 0)
// First time: set the enabled features to all provided features
if (isFirstLoad) {
if (faceCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
else if (!enabledFeatures.value.includes(0)) enabledFeatures.value.push(0)
if (edgeCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
else if (!enabledFeatures.value.includes(1)) enabledFeatures.value.push(1)
if (vertexCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
else if (!enabledFeatures.value.includes(2)) enabledFeatures.value.push(2)
}
// Add darkened back faces for all face objects to improve cutting planes
let childrenToAdd: Array<MObject3D> = [];
sceneModel.traverse((child: MObject3D) => {
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
if (child.userData[extrasNameKey] === modelName) {
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
// Compute a BVH for faster raycasting (MUCH faster selection)
// @ts-ignore
child.geometry?.computeBoundsTree({indirect: true}); // indirect to avoid changing index order
// TODO: Accelerated raycast for lines and points (https://github.com/gkjohnson/three-mesh-bvh/issues/243)
// TODO: ParallelMeshBVHWorker
// We could implement cutting planes using the stencil buffer:
// https://threejs.org/examples/?q=clipping#webgl_clipping_stencil
// But this is buggy for lots of models, so instead we just draw
@@ -193,112 +291,126 @@ function onModelLoad() {
backChild.material = child.material.clone();
backChild.material.side = BackSide;
backChild.material.color = new Color(0.25, 0.25, 0.25)
backChild.userData.noHit = true;
child.userData.backChild = backChild;
childrenToAdd.push(backChild as MObject3D);
}
}
// if (child.type == 'Line' || child.type == 'LineSegments') {
// child.material.linewidth = 3; // Not supported in WebGL2
// If wide lines are really needed, we need https://threejs.org/examples/?q=line#webgl_lines_fat
// }
if (child.type == 'Points') {
(child.material as any).size = 5;
child.material.needsUpdate = true;
}
}
});
childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child));
scene.queueRender()
// Furthermore...
// Enabled features may have been reset after a reload
onEnabledFeaturesChange(enabledFeatures.value)
// Opacity may have been reset after a reload
onOpacityChange(opacity.value)
// Wireframe may have been reset after a reload
onWireframeChange(wireframe.value)
// Clip planes may have been reset after a reload
onClipPlanesChange()
// Edge width may have been reset after a reload
onEdgeWidthChange(edgeWidth.value)
scene.queueRender()
}
// props.viewer.elem may not yet be available, so we need to wait for it
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>
<template>
<v-expansion-panel :value="modelName">
<v-expansion-panel-title expand-icon="hide-this-icon" collapse-icon="hide-this-icon">
<v-btn-toggle v-model="enabledFeatures" multiple @click.stop color="surface-light">
<v-expansion-panel-title collapse-icon="hide-this-icon" expand-icon="hide-this-icon">
<v-btn-toggle v-model="enabledFeatures" color="surface-light" multiple @click.stop>
<v-btn icon>
<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 icon>
<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 icon>
<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-toggle>
<div class="model-name">{{ modelName }}</div>
<v-spacer></v-spacer>
<v-btn icon @click.stop="emit('remove')">
<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-expansion-panel-title>
<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>
<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>
</v-slider>
<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>
<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
</template>
<template v-slot:append>
<v-tooltip activator="parent">Swap clip plane X</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedX">
<v-checkbox-btn v-model="clipPlaneSwappedX" falseIcon="mdi-checkbox-blank-outline"
trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template>
</v-checkbox-btn>
</template>
</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>
<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
</template>
<template v-slot:append>
<v-tooltip activator="parent">Swap clip plane Y</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedY">
<v-checkbox-btn v-model="clipPlaneSwappedZ" falseIcon="mdi-checkbox-blank-outline"
trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template>
</v-checkbox-btn>
</template>
</v-slider>
<v-slider v-model="clipPlaneZ" hide-details min="0" max="1">
<v-slider v-model="clipPlaneY" hide-details max="1" min="0">
<template v-slot:prepend>
<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
</template>
<template v-slot:append>
<v-tooltip activator="parent">Swap clip plane Z</v-tooltip>
<v-checkbox-btn trueIcon="mdi-checkbox-marked-outline" falseIcon="mdi-checkbox-blank-outline"
v-model="clipPlaneSwappedZ">
<v-checkbox-btn v-model="clipPlaneSwappedY" falseIcon="mdi-checkbox-blank-outline"
trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label>
<svg-icon type="mdi" :path="mdiSwapHorizontal"></svg-icon>
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template>
</v-checkbox-btn>
</template>
@@ -332,11 +444,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
}
.model-name {
width: 130px;
min-height: 1.15em; /* HACK: Avoid eating the bottom of the text when using 1 line */
max-height: 2em;
width: 179px;
font-size: 110%;
overflow-x: clip;
overflow-y: visible; /* HACK: bottom of text is lost otherwise (due to buggy -webkit-box bounds?) */
word-wrap: break-word;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2; /* https://caniuse.com/?search=line-clamp */
-webkit-box-orient: vertical;
@@ -359,4 +472,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
.mdi-checkbox-marked-outline { /* HACK: mdi is not fully imported, only required icons... */
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z"/></svg>');
}
.mdi-triangle { /* HACK: mdi is not fully imported, only required icons... */
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M1 21h22L12 2"/></svg>');
}
.mdi-triangle-outline { /* HACK: mdi is not fully imported, only required icons... */
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 2L1 21h22M12 6l7.53 13H4.47"/></svg>');
}
</style>

View File

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

2
frontend/shims.d.ts vendored
View File

@@ -1,3 +1,5 @@
// Avoids typescript error when importing some files
declare module '@jamescoyle/vue-icon'
declare module 'three-orientation-gizmo/src/OrientationGizmo'
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
// License text for all dependencies, only downloaded when/if needed
// @ts-ignore
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 type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
import type {ModelViewerElement} from '@google/model-viewer';
// Optimized minimal dependencies from three
import {Vector3} from "three/src/math/Vector3.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<{ elem: ModelViewerElement | null, scene: ModelScene }>();
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> }>();
function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement {
// noinspection SpellCheckingInspection
@@ -33,21 +31,26 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen
}
// Append and listen for events
gizmo.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => {
let lookFrom = scene.getCamera().position.clone();
let lookAt = scene.getTarget().clone().add(scene.target.position);
let magnitude = lookFrom.clone().sub(lookAt).length()
let direction = new Vector3(axis.direction.x, axis.direction.y, axis.direction.z);
let newLookFrom = lookAt.clone().add(direction.clone().multiplyScalar(magnitude));
//console.log("New camera position", newLookFrom)
scene.getCamera().position.copy(newLookFrom);
scene.getCamera().lookAt(lookAt);
if ((scene as any).__perspectiveCamera) { // HACK: Make the hacky ortho also work
(scene as any).__perspectiveCamera.position.copy(newLookFrom);
(scene as any).__perspectiveCamera.lookAt(lookAt);
if (!props.viewer.elem || !props.viewer.controls) return;
// Animate the controls to the new wanted angle
const controls = props.viewer.controls;
const {theta: curTheta/*, phi: curPhi*/} = (controls as any).goalSpherical;
let wantedTheta = NaN;
let wantedPhi = NaN;
let attempt = 0
while ((attempt == 0 || curTheta == wantedTheta) && attempt < 2) {
if (attempt > 0) { // Flip the camera if the user clicks on the same axis
axis.direction.x = -axis.direction.x;
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();
requestIdleCallback(() => props.elem?.dispatchEvent(
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100})
}
return gizmo;
}
@@ -65,9 +68,9 @@ function updateGizmo() {
}
let reinstall = () => {
if(!container.value) return;
if (!container.value) return;
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);
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 {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
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 {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
import type {Intersection, Material, Mesh, Object3D} from "three";
import {Box3, Matrix4, Raycaster, Vector3} from "three";
import {Box3, Color, Raycaster, Vector3} from "three";
import type ModelViewerWrapperT from "../viewer/ModelViewerWrapper.vue";
import {extrasNameKey} from "../misc/gltf";
import {SceneMgr} from "../misc/scene";
import {Document} from "@gltf-transform/core";
import {AxesColors} from "../misc/helpers";
import {distances} from "../misc/distances";
import {highlight, highlightUndo, hitToSelectionInfo, type SelectionInfo} from "./selection";
export type MObject3D = Mesh & {
userData: { noHit?: boolean },
material: Material & { color: { r: number, g: number, b: number }, __prevBaseColorFactor?: [number, number, number] }
material: Material & {
color: Color,
wireframe?: boolean
}
};
let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
let emit = defineEmits<{ findModel: [string] }>();
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
let selectionEnabled = ref(false);
let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
let selected = defineModel<Array<SelectionInfo>>({default: []});
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
let showDistances = ref<Boolean>(true);
let mouseDownAt: [number, number] | null = null;
let mouseDownTime = 0;
let selectFilter = ref('Any (S)');
const raycaster = new Raycaster();
let selectionMoveListener = (event: MouseEvent) => {
let mouseDownListener = (event: MouseEvent) => {
mouseDownAt = [event.clientX, event.clientY];
mouseDownTime = performance.now();
if (!selectionEnabled.value) return;
};
let selectionListener = (event: MouseEvent) => {
let mouseUpListener = (event: MouseEvent) => {
// If the mouse moved while clicked (dragging), avoid selection logic
if (mouseDownAt) {
let [x, y] = mouseDownAt;
mouseDownAt = null;
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) {
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5 || performance.now() - mouseDownTime > 500) {
return;
}
}
@@ -92,8 +98,27 @@ let selectionListener = (event: MouseEvent) => {
// let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
// setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
// Find all hit objects and select the wanted one based on the filter
const hits = raycaster.intersectObject(scene, true);
// Find all hit objects and raycast the wanted ones based on the filter
let objects: Array<any> = [];
scene.traverse((obj) => {
const kind = obj.type
let isFace = kind === 'Mesh' || kind === 'SkinnedMesh';
let isEdge = kind === 'Line' || kind === 'LineSegments';
let isVertex = kind === 'Points';
if (obj.userData.noHit !== true &&
((selectFilter.value === 'Any (S)' && (isFace || isEdge || isVertex)) ||
(selectFilter.value === '(F)aces' && isFace) ||
(selectFilter.value === '(E)dges' && isEdge) ||
(selectFilter.value === '(V)ertices' && isVertex))) {
objects.push(obj);
}
});
//console.log("Raycasting objects", objects)
// Run the raycaster on the selected objects only searching for the first hit
// @ts-ignore
raycaster.firstHitOnly = true;
const hits = raycaster.intersectObjects(objects, false);
let hit = hits
// Check feasibility
.filter((hit: Intersection<Object3D>) => {
@@ -106,7 +131,7 @@ let selectionListener = (event: MouseEvent) => {
(isFace && selectFilter.value === '(F)aces') ||
(isEdge && selectFilter.value === '(E)dges') ||
(isVertex && selectFilter.value === '(V)ertices');
return (!isFace || hit.object.visible) && !hit.object.userData.noHit && kindOk;
return (!isFace || hit.object.visible) && kindOk;
})
// Sort for highlighting partially hidden edges/vertices
.sort((a, b) => {
@@ -123,17 +148,19 @@ let selectionListener = (event: MouseEvent) => {
})
// Return the best hit
[0] as Intersection<MObject3D> | undefined;
// console.log('Hit', hit)
if (!highlightNextSelection.value[0]) {
// If we are selecting, toggle the selection or deselect all if no hit
if (hit) {
let selInfo: SelectionInfo | null = null;
if (hit) selInfo = hitToSelectionInfo(hit);
//console.log('Hit', hit, 'SelInfo', selInfo);
if (hit && selInfo !== null) {
// Toggle selection
const wasSelected = selected.value.find((m) => m.object.name === hit?.object?.name) !== undefined;
const wasSelected = selected.value.find((m) => m.getKey() === selInfo.getKey()) !== undefined;
if (wasSelected) {
deselect(hit)
deselect(selInfo)
} else {
select(hit)
select(selInfo)
}
} else {
deselectAll();
@@ -149,34 +176,22 @@ let selectionListener = (event: MouseEvent) => {
scene.queueRender() // Force rerender of model-viewer
}
function select(hit: Intersection<MObject3D>) {
// console.log('Selecting', hit.object.name)
if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) {
selected.value.push(hit);
function select(selInfo: SelectionInfo) {
// console.log('Selecting', selInfo.object.name)
if (selected.value.find((m) => m.getKey() === selInfo.getKey()) === undefined) {
selected.value.push(selInfo);
}
hit.object.material.__prevBaseColorFactor = [
hit.object.material.color.r,
hit.object.material.color.g,
hit.object.material.color.b,
];
hit.object.material.color.r = 1;
hit.object.material.color.g = 0;
hit.object.material.color.b = 0;
highlight(selInfo);
}
function deselect(hit: Intersection<MObject3D>, alsoRemove = true) {
// console.log('Deselecting', hit.object.name)
function deselect(selInfo: SelectionInfo, alsoRemove = true) {
// console.log('Deselecting', selInfo.object.name)
if (alsoRemove) {
// Remove the matching object from the selection
let toRemove = selected.value.findIndex((m) => m.object.name === hit.object.name);
let toRemove = selected.value.findIndex((m) => m.getKey() === selInfo.getKey());
selected.value.splice(toRemove, 1);
}
if (hit.object.material.__prevBaseColorFactor) {
hit.object.material.color.r = hit.object.material.__prevBaseColorFactor[0]
hit.object.material.color.g = hit.object.material.__prevBaseColorFactor[1]
hit.object.material.color.b = hit.object.material.__prevBaseColorFactor[2]
delete hit.object.material.__prevBaseColorFactor;
}
highlightUndo(selInfo);
}
function deselectAll(alsoRemove = true) {
@@ -241,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
viewer.onElemReady((elem: ModelViewerElement) => {
if (hasListeners) return;
hasListeners = true;
elem.addEventListener('mouseup', selectionListener);
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
elem.addEventListener('load', () => {
elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
elem.addEventListener('mouseup', mouseUpListener);
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) {
toggleShowBoundingBox();
firstLoad = false;
} else {
updateBoundingBox();
}
});
elem.addEventListener('camera-change', onCameraChange);
@@ -273,9 +303,8 @@ function updateBoundingBox() {
if (selected.value.length > 0) {
bb = new Box3();
for (let hit of selected.value) {
bb.expandByObject(hit.object);
bb.union(hit.getBox())
}
bb.applyMatrix4(new Matrix4().makeTranslation(props.viewer?.scene.getTarget()));
} else {
let boundingBox = SceneMgr.getBoundingBox(sceneDocument.value);
if (!boundingBox) return; // No models. Should not happen.
@@ -380,9 +409,7 @@ function updateDistances() {
}
// Add lines (if not already added)
let objA = selected.value[0].object;
let objB = selected.value[1].object;
let {min, center, max} = distances(objA, objB, props.viewer?.scene);
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
@@ -396,6 +423,8 @@ function updateDistances() {
return;
}
defineExpose({deselect, updateBoundingBox, updateDistances});
// Add keyboard shortcuts
window.addEventListener('keydown', (event) => {
if (event.key === 's') {
@@ -433,52 +462,41 @@ window.addEventListener('keydown', (event) => {
</script>
<template>
<div class="select-parent">
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiCursorDefaultClick"/>
</v-btn>
<v-tooltip :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()" :open-on-click="false">
<template v-slot:activator="{ props }">
<v-select v-bind="props" class="select-only" variant="underlined"
:items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']"
v-model="selectFilter"/>
</template>
<v-btn :color="selectionEnabled ? 'surface-light' : ''" icon @click="toggleSelection">
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
</v-tooltip>
</div>
<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>
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
<svg-icon :path="mdiCursorDefaultClick" type="mdi"/>
</v-btn>
<v-btn icon @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
<v-tooltip :open-on-click="false" :text="'Select only ' + selectFilter.toString().toLocaleLowerCase()">
<template v-slot:activator="{ props }">
<v-select v-model="selectFilter" :items="['Any (S)', '(F)aces', '(E)dges', '(V)ertices']" class="select-only"
v-bind="props"
variant="underlined"/>
</template>
</v-tooltip>
<v-btn :color="highlightNextSelection[0] ? 'surface-light' : ''" icon @click="toggleHighlightNextSelection">
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
<svg-icon :path="mdiFeatureSearch" type="mdi"/>
</v-btn>
<v-btn :color="showBoundingBox ? 'surface-light' : ''" icon @click="toggleShowBoundingBox">
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection (b)ounds' : 'Show selection (b)ounds' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiCubeOutline"/>
<svg-icon :path="mdiCubeOutline" type="mdi"/>
</v-btn>
<v-btn icon @click="toggleShowDistances" :color="showDistances ? 'surface-light' : ''">
<v-btn :color="showDistances ? 'surface-light' : ''" icon @click="toggleShowDistances">
<v-tooltip activator="parent">
{{ showDistances ? 'Hide selection (d)istances' : 'Show (d)istances (when a pair of features is selected)' }}
</v-tooltip>
<svg-icon type="mdi" :path="mdiRuler"/>
<svg-icon :path="mdiRuler" type="mdi"/>
</v-btn>
</template>
<style scoped>
/* Very hacky styling... */
.select-parent {
height: 48px;
}
.select-parent .v-btn {
position: relative;
top: -20px;
}
.select-only {
display: inline-block;
width: calc(100% - 48px);
float: right;
height: 36px;
position: relative;
top: -12px;
width: calc(100% - 48px);
}
</style>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import {
VBtn,
VCard,
@@ -16,11 +16,10 @@ import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon';
import type {ModelViewerElement} from '@google/model-viewer';
import type {Intersection} from "three";
import type {MObject3D} from "./Selection.vue";
import Loading from "../misc/Loading.vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {defineAsyncComponent, type Ref, ref} from "vue";
import {defineAsyncComponent, ref, type Ref} from "vue";
import type {SelectionInfo} from "./selection";
const SelectionComponent = defineAsyncComponent({
loader: () => import("./Selection.vue"),
@@ -39,10 +38,10 @@ const LicensesDialogContent = defineAsyncComponent({
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
const emit = defineEmits<{ findModel: [string] }>()
let selection: Ref<Array<Intersection<MObject3D>>> = ref([]);
let selectionFaceCount = () => selection.value.filter((s) => s.object.type == "Mesh" || s.object.type == "SkinnedMesh").length
let selectionEdgeCount = () => selection.value.filter((s) => s.object.type == "Line").length
let selectionVertexCount = () => selection.value.filter((s) => s.object.type == "Points").length
let selection: Ref<Array<SelectionInfo>> = ref([]);
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
function syncOrthoCamera(force: boolean) {
let scene = props.viewer?.scene;
@@ -57,14 +56,14 @@ function syncOrthoCamera(force: boolean) {
let h = perspectiveWidthAtCenter / scene.aspect;
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
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
requestAnimationFrame(() => syncOrthoCamera(false));
}
}
let toggleProjectionText = ref('PERSP'); // Default to perspective camera
function toggleProjection() {
async function toggleProjection() {
let scene = props.viewer?.scene;
if (!scene) return;
let prevCam = scene.camera;
@@ -79,16 +78,16 @@ function toggleProjection() {
scene.queueRender() // Force rerender of model-viewer
}
toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP';
// The camera change may take a few frames to take effect, dispatch the event after a delay
requestIdleCallback(() => props.viewer?.elem?.dispatchEvent(
new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100})
// The camera change may take a frame to take effect, dispatch the event after a delay
await new Promise((resolve) => requestAnimationFrame(resolve));
props.viewer?.elem?.dispatchEvent(new CustomEvent('camera-change', {detail: {source: 'none'}}));
}
async function centerCamera() {
let viewerEl: ModelViewerElement | null | undefined = props.viewer?.elem;
if (!viewerEl) return;
await viewerEl.updateFraming();
viewerEl.zoom(3);
props.viewer?.scene?.setTarget(0, 0, 0); // Center the target
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')
}
function removeObjectSelections(objName: string) {
for (let selInfo of selection.value.filter((s) => s.getObjectName() === objName)) {
selectionComp.value?.deselect(selInfo);
}
selectionComp.value?.updateBoundingBox();
selectionComp.value?.updateDistances();
}
defineExpose({removeObjectSelections});
// Add keyboard shortcuts
window.addEventListener('keydown', (event) => {
@@ -118,35 +126,35 @@ window.addEventListener('keydown', (event) => {
</script>
<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/>
<h5>Camera</h5>
<v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span>
<v-tooltip activator="parent">Toggle (P)rojection<br/>(currently
{{ toggleProjectionText === 'PERSP' ? 'perspective' : 'orthographic' }})
</v-tooltip>
<svg-icon type="mdi" :path="mdiProjector"></svg-icon>
<svg-icon :path="mdiProjector" type="mdi"></svg-icon>
</v-btn>
<v-btn icon @click="centerCamera">
<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-divider/>
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
<selection-component :ref="selectionComp as any" :viewer="props.viewer as any" v-model="selection"
<selection-component ref="selectionComp" v-model="selection" :viewer="props.viewer as any"
@findModel="(name) => emit('findModel', name)"/>
<v-divider/>
<v-spacer></v-spacer>
<h5>Extras</h5>
<v-btn icon @click="downloadSceneGlb">
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
<svg-icon type="mdi" :path="mdiDownload"/>
<svg-icon :path="mdiDownload" type="mdi"/>
</v-btn>
<v-dialog id="licenses-dialog" fullscreen>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
<v-tooltip activator="parent">Show Licenses</v-tooltip>
<svg-icon type="mdi" :path="mdiLicense"/>
<svg-icon :path="mdiLicense" type="mdi"/>
</v-btn>
</template>
<template v-slot:default="{ isActive }">
@@ -156,7 +164,7 @@ window.addEventListener('keydown', (event) => {
<v-spacer>
</v-spacer>
<v-btn icon @click="isActive.value = false">
<svg-icon type="mdi" :path="mdiClose"/>
<svg-icon :path="mdiClose" type="mdi"/>
</v-btn>
</v-toolbar>
<v-card-text>
@@ -167,7 +175,7 @@ window.addEventListener('keydown', (event) => {
</v-dialog>
<v-btn icon @click="openGithub">
<v-tooltip activator="parent">Open (G)itHub</v-tooltip>
<svg-icon type="mdi" :path="mdiGithub"/>
<svg-icon :path="mdiGithub" type="mdi"/>
</v-btn>
<div ref="statsHolder"></div>
</template>
@@ -187,4 +195,8 @@ window.addEventListener('keydown', (event) => {
position: relative;
top: 5px;
}
h5 {
font-size: 14px;
}
</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 {inject, onMounted, type Ref, ref, watch} from "vue";
import {VList, VListItem} from "vuetify/lib/components/index.mjs";
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
import Loading from "../misc/Loading.vue";
import {$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 type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
import type {Vector3} from "three";
import {BufferGeometry, Mesh} from "three";
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
const emit = defineEmits<{ load: [] }>()
//@ts-ignore
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
//@ts-ignore
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
//@ts-ignore
Mesh.prototype.raycast = acceleratedRaycast;
const props = defineProps<{ src: string }>();
const elem = ref<ModelViewerElement | null>(null);
const scene = ref<ModelScene | 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(() => {
if (!elem.value) return;
elem.value.addEventListener('load', async () => {
elem.value.addEventListener('before-render', () => {
if (!elem.value) return;
// Delete the initial load banner
let banner = elem.value.querySelector('.initial-load-banner');
if (banner) banner.remove();
// Set the scene and renderer
// Extract internals of model-viewer in order to hack unsupported features
scene.value = elem.value[$scene] as ModelScene;
renderer.value = elem.value[$renderer] as Renderer;
// Emit the load event
emit('load')
controls.value = (elem.value as any)[$controls] as SmoothControls;
// 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('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 {
startHotspot: HTMLElement = document.body
endHotspot: HTMLElement = document.body
@@ -88,13 +162,6 @@ function removeLine3D(id: number): boolean {
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);
function onCameraChangeLine(lineId: number) {
@@ -130,48 +197,47 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
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')!!;
watch(disableTap, (value) => {
// Rerender not auto triggered? This works anyway...
if (value) elem.value?.setAttribute('disable-tap', '');
else elem.value?.removeAttribute('disable-tap');
watch(disableTap, (newDisableTap) => {
if (elem.value) elem.value.disableTap = newDisableTap;
});
</script>
<template>
<!-- The main 3D model viewer -->
<model-viewer ref="elem" style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto"
min-camera-orbit="-Infinity 0deg 5%" :disable-tap="disableTap" :exposure="settings.exposure"
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
:environment-image="settings.background">
<slot></slot> <!-- Controls, annotations, etc. -->
<div class="annotation initial-load-banner">
Trying to load models from...
<v-list v-for="src in settings.preload" :key="src">
<v-list-item>{{ src }}</v-list-item>
</v-list>
<loading></loading>
<model-viewer ref="elem" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :autoplay="settings.autoplay"
:environment-image="settings.background" :exposure="settings.exposure"
:orbit-sensitivity="settings.orbitSensitivity" :pan-sensitivity="settings.panSensitivity"
:poster="poster" :shadow-intensity="settings.shadowIntensity" :skybox-image="settings.background"
:src="props.src" :zoom-sensitivity="settings.zoomSensitivity" alt="The 3D model(s)" camera-controls
camera-orbit="30deg 75deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
min-camera-orbit="-Infinity 0deg 5%" style="width: 100%; height: 100%">
<slot></slot>
<!-- Add a progress bar to the top of the model viewer -->
<div ref="progressBar" slot="progress-bar" class="progress-bar">
<div ref="updateBar" class="update-bar"/>
</div>
</model-viewer>
<!-- The SVG overlay for fake 3D lines attached to the model -->
<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">
<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"/>
<g v-if="line.centerText !== undefined">
<rect :x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"
:y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2"
:width="line.centerTextSize[0] + 8" :height="line.centerTextSize[1] + 4"
fill="gray" fill-opacity="0.75" rx="4" ry="4" stroke="black" v-if="line.centerText"/>
<text :x="(line.start2D[0] + line.end2D[0]) / 2" :y="(line.start2D[1] + line.end2D[1]) / 2"
text-anchor="middle" dominant-baseline="middle" font-size="16" fill="black"
:class="'line' + lineId + '_text'" v-if="line.centerText">
<rect v-if="line.centerText"
:height="line.centerTextSize[1] + 4"
:width="line.centerTextSize[0] + 8"
:x="(line.start2D[0] + line.end2D[0]) / 2 - line.centerTextSize[0]/2 - 4"
:y="(line.start2D[1] + line.end2D[1]) / 2 - line.centerTextSize[1]/2 - 2" fill="gray"
fill-opacity="0.75" rx="4" ry="4" stroke="black"/>
<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 }}
</text>
</g>
@@ -206,14 +272,29 @@ watch(disableTap, (value) => {
pointer-events: none;
}
.initial-load-banner {
width: 300px;
margin: auto;
margin-top: 3em;
overflow: hidden;
.progress-bar {
display: block;
pointer-events: none;
width: 100%;
height: 10%;
max-height: 2%;
position: absolute;
left: 50%;
top: 0;
transform: translate3d(-50%, 0%, 0);
border-radius: 25px;
box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.5), 0 0 5px 1px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.9);
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
}
.initial-load-banner .v-list-item {
overflow: hidden;
.update-bar {
background-color: rgba(255, 255, 255, 0.9);
width: 0;
height: 100%;
border-radius: 25px;
float: left;
transition: width 0.3s;
}
</style>

View File

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

135
poetry.lock generated
View File

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

View File

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

View File

@@ -26,8 +26,10 @@ export default defineConfig({
}
},
build: {
assetsDir: '.',
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
cssCodeSplit: false, // Small enough to inline
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
sourcemap: true, // For debugging production
},
define: {
__APP_NAME__: JSON.stringify(name),

View File

@@ -1,6 +1,6 @@
import os
from cad import image_to_gltf
from yacv_server.cad import image_to_gltf
from yacv_server.yacv import YACV
yacv = YACV()
@@ -15,7 +15,7 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
# Expose some nice aliases using the default server instance
show = yacv.show
show_all = yacv.show_cad_all
prepare_image = image_to_gltf
image_to_gltf = image_to_gltf
export_all = yacv.export_all
remove = yacv.remove
clear = yacv.clear

View File

@@ -2,10 +2,15 @@
Utilities to work with CAD objects
"""
import hashlib
from typing import Optional, Union, List, Tuple
import io
import re
from typing import Optional, Union, Tuple
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Shape
from build123d import Compound, Shape
from yacv_server.gltf import GLTFMgr
@@ -40,27 +45,48 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
if isinstance(obj, TopoDS_Shape):
return obj
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, set) or isinstance(obj, dict):
try:
if isinstance(obj, dict):
obj_iter = iter(obj.values())
else:
obj_iter = iter(obj)
# print(obj, ' -> ', obj_iter)
shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
# Silently drop non-shapes
shapes_raw_filtered = [shape for shape in shapes_raw if shape is not None]
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
# Sorting is required to improve hashcode consistency
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
# Build a single compound shape
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
return get_shape(Compound(shapes_bd), error)
except TypeError:
pass
if error:
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
else:
return None
def grab_all_cad() -> List[Tuple[str, CADCoreLike]]:
def grab_all_cad() -> set[Tuple[str, CADCoreLike]]:
""" Grab all shapes by inspecting the stack """
import inspect
stack = inspect.stack()
shapes = []
shapes = set()
for frame in stack:
for key, value in frame.frame.f_locals.items():
shape = get_shape(value, error=False)
if shape:
shapes.append((key, shape))
if shape and shape not in shapes:
shapes.add((key, shape))
return shapes
def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None,
name: Optional[str] = None, save_mime: str = 'image/jpeg') -> Tuple[bytes, str]:
name: Optional[str] = None, save_mime: str = 'image/jpeg', power_of_two: bool = True) \
-> Tuple[bytes, str]:
"""Convert an image to a GLTF CAD object."""
from PIL import Image
import io
@@ -90,33 +116,40 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
if not isinstance(center_loc, TopLoc_Location):
raise ValueError('Center location not valid')
plane = Plane(Location(center_loc))
# Convert coordinates system
plane.origin = Vector(plane.origin.X, plane.origin.Z, -plane.origin.Y)
plane.z_dir = -plane.y_dir
plane.y_dir = plane.z_dir
def vert(v: Vector) -> Tuple[float, float, float]:
return v.X, v.Y, v.Z
# Load the image to a byte buffer
img = Image.open(source)
img_buf = io.BytesIO()
img.save(img_buf, format=format)
img_buf = img_buf.getvalue()
# Build the gltf
mgr = GLTFMgr(image=(img_buf, save_mime))
# Use the original dimensions for scaling the model
if width is None and height is None:
raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm?
elif width is None:
width = img.width / img.height * height
elif height is None:
height = height or img.height / img.width * width # Apply default aspect ratio if unspecified
# Resize the image to a power of two if requested (recommended for GLTF)
if power_of_two:
new_width = 2 ** (img.width - 1).bit_length()
new_height = 2 ** (img.height - 1).bit_length()
img = img.resize((new_width, new_height))
# Save the image to a buffer
img.save(img_buf, format=format)
img_buf = img_buf.getvalue()
# Convert coordinates system as a last step (gltf is Y-up instead of Z-up)
def vert(v: Vector) -> Vector:
return Vector(v.X, v.Z, -v.Y)
# Build the gltf
mgr = GLTFMgr(image=(img_buf, save_mime))
mgr.add_face([
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
vert(plane.origin - plane.x_dir * width / 2 + plane.y_dir * height / 2),
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
], [
(0, 2, 1),
(0, 3, 2),
@@ -125,7 +158,36 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
(1, 0),
(1, 1),
(0, 1),
])
], (1, 1, 1, 1))
# Return the GLTF binary blob and the suggested name of the image
return b''.join(mgr.gltf.save_to_bytes()), name
return b''.join(mgr.build().save_to_bytes()), name
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
"""Utility to compute the STABLE hash code of a shape"""
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
# This is best-effort and not guaranteed to be unique
hasher = hashlib.md5(usedforsecurity=False)
for k, v in extras.items():
hasher.update(str(k).encode())
hasher.update(str(v).encode())
if isinstance(obj, bytes):
hasher.update(obj)
elif isinstance(obj, TopLoc_Location):
sub_data = io.BytesIO()
obj.DumpJson(sub_data)
hasher.update(sub_data.getvalue())
elif isinstance(obj, TopoDS_Shape):
map_of_shapes = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(obj, map_of_shapes)
for i in range(1, map_of_shapes.Extent() + 1):
sub_shape = map_of_shapes.FindKey(i)
sub_data = io.BytesIO()
TopoDS_Shape.DumpJson(sub_shape, sub_data)
val = sub_data.getvalue()
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
hasher.update(val)
else:
raise ValueError(f'Cannot hash object of type {type(obj)}')
return hasher.hexdigest()

View File

@@ -12,43 +12,95 @@ _checkerboard_image_bytes = base64.decodebytes(
class GLTFMgr:
"""A utility class to build our GLTF2 objects easily and incrementally"""
def __init__(self, image: Tuple[bytes, str] = (_checkerboard_image_bytes, 'image/png')):
gltf: GLTF2
# Intermediate data to be filled by the add_* methods and merged into the GLTF object
# - Face data
face_indices: List[int] # 3 indices per triangle
face_positions: List[float] # x, y, z
face_tex_coords: List[float] # u, v
face_colors: List[float] # r, g, b, a
image: Optional[Tuple[bytes, str]] # image/png
# - Edge data
edge_indices: List[int] # 2 indices per edge
edge_positions: List[float] # x, y, z
edge_colors: List[float] # r, g, b, a
# - Vertex data
vertex_indices: List[int] # 1 index per vertex
vertex_positions: List[float] # x, y, z
vertex_colors: List[float] # r, g, b, a
def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')):
self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
scene=0,
scenes=[Scene(nodes=[0])],
nodes=[Node(mesh=0)],
meshes=[Mesh(primitives=[])],
accessors=[],
bufferViews=[BufferView(buffer=0, byteLength=len(image[0]), byteOffset=0)],
buffers=[Buffer(byteLength=len(image[0]))],
samplers=[Sampler(magFilter=NEAREST)],
textures=[Texture(source=0, sampler=0)],
images=[Image(bufferView=0, mimeType=image[1])],
nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
meshes=[Mesh(primitives=[
Primitive(indices=-1, attributes=Attributes(), mode=TRIANGLES, material=0,
extras={"face_triangles_end": []}),
Primitive(indices=-1, attributes=Attributes(), mode=LINES, material=0,
extras={"edge_points_end": []}),
Primitive(indices=-1, attributes=Attributes(), mode=POINTS, material=0),
])],
materials=[Material(pbrMetallicRoughness=PbrMetallicRoughness(metallicFactor=0.1, roughnessFactor=1.0),
alphaCutoff=None)],
)
self.gltf.set_binary_blob(image[0])
self.face_indices = []
self.face_positions = []
self.face_tex_coords = []
self.face_colors = []
self.image = image
self.edge_indices = []
self.edge_positions = []
self.edge_colors = []
self.vertex_indices = []
self.vertex_positions = []
self.vertex_colors = []
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]]):
"""Add a face to the GLTF as a new primitive of the unique mesh"""
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
indices = np.array([[i[0], i[1], i[2]] for i in indices_raw], dtype=np.uint32)
tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32)
self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material="face")
@property
def _faces_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == TRIANGLES][0]
def add_edge(self, vertices_raw: List[Tuple[float, float, float]], mat: str = None):
"""Add an edge to the GLTF as a new primitive of the unique mesh"""
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32)
tex_coord = np.array([])
self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material=mat or "edge")
@property
def _edges_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == LINES][0]
def add_vertex(self, vertex: Tuple[float, float, float]):
"""Add a vertex to the GLTF as a new primitive of the unique mesh"""
vertices = np.array([[vertex[0], vertex[1], vertex[2]]])
indices = np.array([[0]], dtype=np.uint32)
tex_coord = np.array([], dtype=np.float32)
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex")
@property
def _vertices_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]],
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
"""Add a face to the GLTF mesh"""
# assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths"
# assert min([i for t in indices_raw for i in t]) == 0, f"Face indices start at {min(indices_raw)}"
# assert max([e for t in indices_raw for e in t]) < len(vertices_raw), f"Indices have non-existing vertices"
base_index = len(self.face_positions) // 3 # All the new indices reference the new vertices
self.face_indices.extend([base_index + i for t in indices_raw for i in t])
self.face_positions.extend([v for t in vertices_raw for v in t])
self.face_tex_coords.extend([c for t in tex_coord_raw for c in t])
self.face_colors.extend([col for _ in range(len(vertices_raw)) for col in color])
self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices))
def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
color: Tuple[float, float, float, float] = (0.1, 0.1, 1.0, 1.0)):
"""Add an edge to the GLTF mesh"""
vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc.
base_index = len(self.edge_positions) // 3
self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))])
self.edge_positions.extend([v for t in vertices_flat for v in t])
self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color])
self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices))
def add_vertex(self, vertex: Tuple[float, float, float],
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.1, 1.0)):
"""Add a vertex to the GLTF mesh"""
base_index = len(self.vertex_positions) // 3
self.vertex_indices.append(base_index)
self.vertex_positions.extend(vertex)
self.vertex_colors.extend(color)
def add_location(self, loc: Location):
"""Add a location to the GLTF as a new primitive of the unique mesh"""
@@ -59,120 +111,91 @@ class GLTFMgr:
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
self.add_vertex(vert(pl.origin))
self.add_edge([vert(pl.origin), vert(pl.origin + pl.x_dir)], mat="locX")
self.add_edge([vert(pl.origin), vert(pl.origin + pl.y_dir)], mat="locY")
self.add_edge([vert(pl.origin), vert(pl.origin + pl.z_dir)], mat="locZ")
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1))
def add_material(self, kind: str) -> int:
"""It is important to use a different material for each primitive to be able to change them at runtime"""
new_material: Material
if kind == "face":
new_material = Material(name="face", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 1, 0.5, 1]), doubleSided=True)
elif kind == "edge":
new_material = Material(name="edge", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorFactor=[0, 0, 0.5, 1]))
elif kind == "vertex":
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorFactor=[0, 0.3, 0.3, 1]))
elif kind == "locX":
new_material = Material(name="locX", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorFactor=[0.97, 0.24, 0.24, 1]))
elif kind == "locY":
new_material = Material(name="locY", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorFactor=[0.42, 0.8, 0.15, 1]))
elif kind == "locZ":
new_material = Material(name="locZ", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
baseColorFactor=[0.09, 0.55, 0.94, 1]))
def build(self) -> GLTF2:
"""Merge the intermediate data into the GLTF object and return it"""
buffers_list: List[Tuple[Accessor, BufferView, bytes]] = []
if len(self.face_indices) > 0:
self._faces_primitive.indices = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
self._faces_primitive.attributes.POSITION = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_positions, 3))
self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
self._faces_primitive.attributes.COLOR_0 = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_colors, 4))
else:
raise ValueError(f"Unknown material kind {kind}")
self.gltf.materials.append(new_material)
return len(self.gltf.materials) - 1
self.image = None # Unused image
self.gltf.meshes[0].primitives = list( # Remove unused faces primitive
filter(lambda p: p.mode != TRIANGLES, self.gltf.meshes[0].primitives))
def _add_any(self, vertices: np.ndarray, indices: np.ndarray, tex_coord: np.ndarray, mode: int = TRIANGLES,
material: str = "face"):
assert vertices.ndim == 2
assert vertices.shape[1] == 3
vertices = vertices.astype(np.float32)
vertices_blob = vertices.tobytes()
edges_and_vertices_mat = 0
if self.image is not None and (len(self.edge_indices) > 0 or len(self.vertex_indices) > 0):
# Create a material without texture for edges and vertices
edges_and_vertices_mat = len(self.gltf.materials)
new_mat = copy.deepcopy(self.gltf.materials[0])
new_mat.pbrMetallicRoughness.baseColorTexture = None
self.gltf.materials.append(new_mat)
assert indices.ndim == 2
assert indices.shape[1] == 3 and mode == TRIANGLES or indices.shape[1] == 2 and mode == LINE_STRIP or \
indices.shape[1] == 1 and mode == POINTS
indices = indices.astype(np.uint32)
indices_blob = indices.flatten().tobytes()
# Treat edges and vertices the same way
for (indices, positions, colors, primitive, kind) in [
(self.edge_indices, self.edge_positions, self.edge_colors, self._edges_primitive, LINES),
(self.vertex_indices, self.vertex_positions, self.vertex_colors, self._vertices_primitive, POINTS)
]:
if len(indices) > 0:
primitive.material = edges_and_vertices_mat
primitive.indices = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(indices, 1))
primitive.attributes.POSITION = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(positions, 3))
primitive.attributes.COLOR_0 = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(colors, 4))
else:
self.gltf.meshes[0].primitives = list( # Remove unused edges primitive
filter(lambda p: p.mode != kind, self.gltf.meshes[0].primitives))
# Check that all vertices are referenced by the indices
# This can happen on broken faces like on some fonts
# assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}"
# assert indices.min() == 0, f"min({indices}) != 0"
# assert np.unique(indices.flatten()).size == len(vertices)
if self.image is not None: # Add texture last as it creates a fake accessor that is not added!
self.gltf.images = [Image(bufferView=len(buffers_list), mimeType=self.image[1])]
self.gltf.textures = [Texture(source=0, sampler=0)]
self.gltf.samplers = [Sampler(magFilter=NEAREST)]
self.gltf.materials[0].pbrMetallicRoughness.baseColorTexture = TextureInfo(index=0)
buffers_list.append((Accessor(), BufferView(), self.image[0]))
assert len(tex_coord) == 0 or tex_coord.ndim == 2
assert len(tex_coord) == 0 or tex_coord.shape[1] == 2
tex_coord = tex_coord.astype(np.float32)
tex_coord_blob = tex_coord.tobytes()
accessor_base = len(self.gltf.accessors)
self.gltf.meshes[0].primitives.append(
Primitive(
attributes=Attributes(POSITION=accessor_base + 1, TEXCOORD_0=accessor_base + 2)
if len(tex_coord) > 0 else Attributes(POSITION=accessor_base + 1),
indices=accessor_base,
mode=mode,
material=self.add_material(material),
)
)
buffer_view_base = len(self.gltf.bufferViews)
self.gltf.accessors.extend([it for it in [
Accessor(
bufferView=buffer_view_base,
componentType=UNSIGNED_INT,
count=indices.size,
type=SCALAR,
max=[int(indices.max())],
min=[int(indices.min())],
),
Accessor(
bufferView=buffer_view_base + 1,
componentType=FLOAT,
count=len(vertices),
type=VEC3,
max=vertices.max(axis=0).tolist(),
min=vertices.min(axis=0).tolist(),
),
Accessor(
bufferView=buffer_view_base + 2,
componentType=FLOAT,
count=len(tex_coord),
type=VEC2,
max=tex_coord.max(axis=0).tolist(),
min=tex_coord.min(axis=0).tolist(),
) if len(tex_coord) > 0 else None
] if it is not None])
prev_binary_blob = self.gltf.binary_blob()
# Once all the data is ready, we can concatenate the buffers updating the accessors and views
prev_binary_blob = self.gltf.binary_blob() or b''
byte_offset_base = len(prev_binary_blob)
self.gltf.bufferViews.extend([bv for bv in [
BufferView(
buffer=0,
byteOffset=byte_offset_base,
byteLength=len(indices_blob),
target=ELEMENT_ARRAY_BUFFER,
),
BufferView(
buffer=0,
byteOffset=byte_offset_base + len(indices_blob),
byteLength=len(vertices_blob),
target=ARRAY_BUFFER,
),
BufferView(
buffer=0,
byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob),
byteLength=len(tex_coord_blob),
target=ARRAY_BUFFER,
)
] if bv.byteLength > 0])
for accessor, bufferView, blob in buffers_list:
self.gltf.set_binary_blob(prev_binary_blob + indices_blob + vertices_blob + tex_coord_blob)
if accessor.componentType is not None: # Remove accessor of texture
buffer_view_base = len(self.gltf.bufferViews)
accessor.bufferView = buffer_view_base
self.gltf.accessors.append(accessor)
bufferView.buffer = 0
bufferView.byteOffset = byte_offset_base
bufferView.byteLength = len(blob)
self.gltf.bufferViews.append(bufferView)
byte_offset_base += len(blob)
prev_binary_blob += blob
self.gltf.buffers.append(Buffer(byteLength=byte_offset_base))
self.gltf.set_binary_blob(prev_binary_blob)
return self.gltf
def _gen_buffer_metadata(data: List[any], chunk: int) -> Tuple[Accessor, BufferView, bytes]:
return Accessor(
componentType={1: UNSIGNED_INT, 2: FLOAT, 3: FLOAT, 4: FLOAT}[chunk],
count=len(data) // chunk,
type={1: SCALAR, 2: VEC2, 3: VEC3, 4: VEC4}[chunk],
max=[max(data[i::chunk]) for i in range(chunk)],
min=[min(data[i::chunk]) for i in range(chunk)],
), BufferView(
target={1: ELEMENT_ARRAY_BUFFER, 2: ARRAY_BUFFER, 3: ARRAY_BUFFER, 4: ARRAY_BUFFER}[chunk],
), np.array(data, dtype={1: np.uint32, 2: np.float32, 3: np.float32, 4: np.float32}[chunk]).tobytes()

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')
extrude(amount=1)
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location # Avoid overlapping:
logo_img_location.position = Vector(logo_img_location.position.X - 4e-2, logo_img_location.position.Y,
logo_img_location.position.Z)
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location
logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
img_bytes, img_name = prepare_image(logo_img_path, logo_img_location, height=18)
img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read()
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_bytes}
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, img_name: img_glb_bytes}
if __name__ == "__main__":
@@ -43,7 +42,7 @@ if __name__ == "__main__":
# If this is not set, the server will auto-start on import and show_* calls will provide live updates
os.environ['YACV_DISABLE_SERVER'] = 'True'
from yacv_server import export_all, remove, prepare_image, show
from yacv_server import export_all, show, image_to_gltf
# Build the CAD part of the logo
logo = build_logo()
@@ -52,7 +51,8 @@ if __name__ == "__main__":
show(*[obj for obj in logo.values()], names=[name for name in logo.keys()])
if testing_server:
remove('location') # Test removing a part
# remove('location') # Test removing a part
pass
else:
# Save the complete logo to multiple GLB files
export_all(os.path.join(ASSETS_DIR, 'logo_build'))

View File

@@ -1,13 +1,10 @@
import io
import os
import threading
import urllib.parse
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
from iterators import TimeoutIterator
from mylogger import logger
from yacv_server.mylogger import logger
# Find the frontend folder (optional, but recommended)
FILE_DIR = os.path.dirname(__file__)
@@ -26,13 +23,9 @@ OBJECTS_API_PATH = '/api/object' # /{name}
class HTTPHandler(SimpleHTTPRequestHandler):
yacv: 'yacv.YACV'
frontend_lock: threading.Lock # To avoid exiting too early while frontend makes requests
at_least_one_client: threading.Event
def __init__(self, *args, yacv: 'yacv.YACV', **kwargs):
self.yacv = yacv
self.frontend_lock = threading.Lock()
self.at_least_one_client = threading.Event()
super().__init__(*args, **kwargs, directory=FRONTEND_BASE_PATH)
def log_message(self, fmt, *args):
@@ -77,69 +70,65 @@ class HTTPHandler(SimpleHTTPRequestHandler):
def _api_updates(self):
"""Handles a publish-only websocket connection that send show_object events along with their hashes and URLs"""
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
# Chunked transfer encoding!
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
self.at_least_one_client.set()
logger.debug('Updates client connected')
def write_chunk(_chunk_data: str):
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
self.wfile.write(b'\r\n')
self.wfile.write(_chunk_data.encode('utf-8'))
self.wfile.write(b'\r\n')
self.wfile.flush()
# Keep a shared read lock to know if any frontend is still working before shutting down
with self.yacv.frontend_lock.r_locked():
write_chunk('retry: 100\n\n')
# Avoid accepting new connections while shutting down
if self.yacv.shutting_down.is_set() and self.yacv.at_least_one_client.is_set():
self.send_error(HTTPStatus.SERVICE_UNAVAILABLE, 'Server is shutting down')
return
self.yacv.at_least_one_client.set()
logger.debug('Updates client connected')
# Send buffered events first, while keeping a lock
with self.frontend_lock:
for data in self.yacv.show_events.buffer():
logger.debug('Sending info about %s: %s', data.name, data)
# noinspection PyUnresolvedReferences
to_send = data.to_json()
write_chunk(f'data: {to_send}\n\n')
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
# Chunked transfer encoding!
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
# Send future events over the same connection
# Also send keep-alive to know if the client is still connected
subscription = self.yacv.show_events.subscribe(include_buffered=False)
it = TimeoutIterator(subscription, sentinel=None, reset_on_next=True, timeout=5.0) # Keep-alive interval
try:
for data in it:
if data is None:
write_chunk(':keep-alive\n\n')
else:
logger.debug('Sending info about %s: %s', data.name, data)
# noinspection PyUnresolvedReferences
to_send = data.to_json()
write_chunk(f'data: {to_send}\n\n')
except BrokenPipeError: # Client disconnected normally
pass
finally:
logger.debug('Updates client disconnected')
def write_chunk(_chunk_data: str):
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
self.wfile.write(b'\r\n')
self.wfile.write(_chunk_data.encode('utf-8'))
self.wfile.write(b'\r\n')
self.wfile.flush()
write_chunk('retry: 100\n\n')
subscription = self.yacv.show_events.subscribe(yield_timeout=1.0) # Keep-alive interval
try:
it.interrupt()
next(it) # Make sure the iterator is interrupted before trying to close the subscription
for data in subscription:
if data is None:
write_chunk(':keep-alive\n\n')
else:
logger.debug('Sending info about %s: %s', data.name, data)
# noinspection PyUnresolvedReferences
to_send = data.to_json()
write_chunk(f'data: {to_send}\n\n')
except BrokenPipeError: # Client disconnected normally
pass
finally:
subscription.close()
except BaseException as e:
logger.debug('Ignoring error while closing subscription: %s', e)
logger.debug('Updates client disconnected')
def _api_object(self, obj_name: str):
"""Returns the object file with the matching name, building it if necessary."""
with self.frontend_lock:
# Export the object (or fail if not found)
exported_glb = self.yacv.export(obj_name)
if exported_glb is None:
self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found')
return io.BytesIO()
# Export the object (or fail if not found)
_export = self.yacv.export(obj_name)
if _export is None:
self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found')
return io.BytesIO()
# Wrap the GLB in a response and return it
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'model/gltf-binary')
self.send_header('Content-Length', str(len(exported_glb)))
self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"')
self.end_headers()
self.wfile.write(exported_glb)
exported_glb, _hash = _export
# Wrap the GLB in a response and return it
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'model/gltf-binary')
self.send_header('Content-Length', str(len(exported_glb)))
self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"')
self.send_header('E-Tag', f'"{_hash}"')
self.end_headers()
self.wfile.write(exported_glb)

View File

@@ -1,4 +1,4 @@
import threading
import queue
import queue
import threading
from typing import List, TypeVar, \
@@ -8,6 +8,8 @@ from yacv_server.mylogger import logger
T = TypeVar('T')
_end_of_queue = object()
class BufferedPubSub(Generic[T]):
"""A simple implementation of publish-subscribe pattern using threading and buffering all previous events"""
@@ -45,7 +47,7 @@ class BufferedPubSub(Generic[T]):
for event in self._buffer:
q.put(event)
if not include_future:
q.put(None)
q.put(_end_of_queue)
return q
def _unsubscribe(self, q: queue.Queue[T]):
@@ -54,14 +56,18 @@ class BufferedPubSub(Generic[T]):
self._subscribers.remove(q)
logger.debug(f"Unsubscribed from %s (%d subscribers)", self, len(self._subscribers))
def subscribe(self, include_buffered: bool = True, include_future: bool = True) -> Generator[T, None, None]:
def subscribe(self, include_buffered: bool = True, include_future: bool = True, yield_timeout: float = 0.0) -> \
Generator[T, None, None]:
"""Subscribes to events as an generator that yields events and automatically unsubscribes"""
q = self._subscribe(include_buffered, include_future)
try:
while True:
v = q.get()
try:
v = q.get(timeout=yield_timeout)
except queue.Empty:
v = None
# include_future is incompatible with None values as they are used to signal the end of the stream
if v is None and not include_future:
if v is _end_of_queue:
break
yield v
finally: # When aclose() is called
@@ -80,4 +86,4 @@ class BufferedPubSub(Generic[T]):
def clear(self):
"""Clears the buffer"""
with self._buffer_lock:
self._buffer.clear()
self._buffer.clear()

96
yacv_server/rwlock.py Normal file
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
import io
import re
from typing import List, Dict, Tuple, Union
from typing import List, Dict, Tuple
from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_TangentialDeflection
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
from build123d import Shape, Vertex, Face, Location
from pygltflib import GLTF2
@@ -36,8 +31,8 @@ def tessellate(
shape = Shape(cad_like)
# Perform tessellation tasks
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {}
edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
if faces:
for face in shape.faces():
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
@@ -55,7 +50,7 @@ def tessellate(
for vertex in shape.vertices():
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
return mgr.gltf
return mgr.build()
def _tessellate_face(
@@ -65,12 +60,12 @@ def _tessellate_face(
angular_tolerance: float = 0.1
):
face = Shape(ocp_face)
face.mesh(tolerance, angular_tolerance)
# face.mesh(tolerance, angular_tolerance)
tri_mesh = face.tessellate(tolerance, angular_tolerance)
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
if poly is None:
logger.warn("No triangulation found for face")
return GLTF2()
tri_mesh = face.tessellate(tolerance, angular_tolerance)
# Get UV of each face from the parameters
uv = [
@@ -78,7 +73,7 @@ def _tessellate_face(
for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1))
]
vertices = [(v.X, v.Y, v.Z) for v in tri_mesh[0]]
vertices = tri_mesh[0]
indices = tri_mesh[1]
mgr.add_face(vertices, indices, uv)
@@ -91,9 +86,9 @@ def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tupl
push_dir = (push_dir[0] + normal.X, push_dir[1] + normal.Y, push_dir[2] + normal.Z)
if push_dir != (0, 0, 0):
# Normalize the push direction by the number of faces and a constant factor
# NOTE: Don't overdo it, or metrics will be wrong
n = len(faces) / 1e-3
push_dir = (push_dir[0] / n, push_dir[1] / n, push_dir[2] / n)
# NOTE: Don't overdo it, or metrics will be (more) wrong
n = 1e-3 / len(faces)
push_dir = (push_dir[0] * n, push_dir[1] * n, push_dir[2] * n)
# Push the vertex by the normal
v = (v[0] + push_dir[0], v[1] + push_dir[1], v[2] + push_dir[2])
return v
@@ -119,6 +114,9 @@ def _tessellate_edge(
for i in range(1, discretizer.NbPoints() + 1)
)
]
# Convert strip of vertices to a list of pairs of vertices
vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)]
mgr.add_edge(vertices)
@@ -127,30 +125,3 @@ def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[Topo
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
# This is best-effort and not guaranteed to be unique
hasher = hashlib.md5(usedforsecurity=False)
for k, v in extras.items():
hasher.update(str(k).encode())
hasher.update(str(v).encode())
if isinstance(obj, bytes):
hasher.update(obj)
elif isinstance(obj, TopLoc_Location):
sub_data = io.BytesIO()
obj.DumpJson(sub_data)
hasher.update(sub_data.getvalue())
elif isinstance(obj, TopoDS_Shape):
map_of_shapes = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(obj, map_of_shapes)
for i in range(1, map_of_shapes.Extent() + 1):
sub_shape = map_of_shapes.FindKey(i)
sub_data = io.BytesIO()
TopoDS_Shape.DumpJson(sub_shape, sub_data)
val = sub_data.getvalue()
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
hasher.update(val)
else:
raise ValueError(f'Cannot hash object of type {type(obj)}')
return hasher.hexdigest()

View File

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

530
yarn.lock
View File

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