mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbeae5632e | ||
|
|
59116e4a1a | ||
|
|
0be0103c3c | ||
|
|
d6deef9e7f | ||
|
|
ad956762f4 | ||
|
|
a4acd2f3d3 | ||
|
|
c877fef490 | ||
|
|
0855a9c6c7 | ||
|
|
657b34d098 | ||
|
|
ad83f1c937 | ||
|
|
38be4c638b | ||
|
|
63f2b716d6 | ||
|
|
9e70a3998d | ||
|
|
c7c4adc250 | ||
|
|
393decd876 | ||
|
|
111f417905 | ||
|
|
7296b15a67 | ||
|
|
88190b0d1e | ||
|
|
f2a607bb00 | ||
|
|
0b6ed2fc34 | ||
|
|
d1e5658e07 | ||
|
|
3545785cae | ||
|
|
1df938b067 | ||
|
|
42ae6384f0 | ||
|
|
b10b228fcc | ||
|
|
ffaf6f26ba | ||
|
|
17357aef25 | ||
|
|
0e6118ed94 | ||
|
|
8a435b5f1a | ||
|
|
a63d018850 | ||
|
|
0460e939e4 | ||
|
|
b54453ca06 | ||
|
|
cc4e3f427e | ||
|
|
fc32393635 | ||
|
|
ae2a5f9bb0 | ||
|
|
46cf45e4ce | ||
|
|
3423c99f8d | ||
|
|
667a08d2c6 | ||
|
|
1733337331 | ||
|
|
0c51b614e9 | ||
|
|
9ef372f576 | ||
|
|
a2275f2897 | ||
|
|
b597b1e3a5 | ||
|
|
4af4315984 | ||
|
|
83bc4c767d | ||
|
|
10ed5e2e9e | ||
|
|
41662944d6 | ||
|
|
7f00a3a4ee | ||
|
|
7549c2543b | ||
|
|
fb7a525a71 | ||
|
|
c14a823dc1 | ||
|
|
3845720d53 | ||
|
|
16c109f399 | ||
|
|
7b58b00231 | ||
|
|
05a159ac6c | ||
|
|
458f81f51d | ||
|
|
f3545bebb8 | ||
|
|
90d05f08c4 | ||
|
|
5b4865978f | ||
|
|
9ec2de8e4a | ||
|
|
0c98c273b8 | ||
|
|
014a68c33d | ||
|
|
16bf550f75 | ||
|
|
f4a74c2f1c | ||
|
|
1fddd984aa | ||
|
|
5809b88cee | ||
|
|
5cd7fb5d9d | ||
|
|
dbfc72c03b | ||
|
|
437e8eb4ad | ||
|
|
7051a71710 | ||
|
|
28282f4b06 | ||
|
|
7f43367459 | ||
|
|
ba64f70300 | ||
|
|
05963d58f2 | ||
|
|
405c061f93 | ||
|
|
e0a00b2d32 | ||
|
|
90f4611ee8 | ||
|
|
ee954622bb | ||
|
|
f5be0618ad | ||
|
|
f92701a969 | ||
|
|
949f92a28f | ||
|
|
7618581ef5 | ||
|
|
5460d19fdb | ||
|
|
f84292e4b7 | ||
|
|
b597223228 | ||
|
|
8ec9a3f507 | ||
|
|
0ec339283e | ||
|
|
1d50dc20ba | ||
|
|
35331746b1 | ||
|
|
287f76c0ad | ||
|
|
dadb2b7a39 | ||
|
|
722432dfb0 | ||
|
|
a9184b224f | ||
|
|
3c732d7c85 | ||
|
|
d8fa1f1e0b | ||
|
|
1b03699677 | ||
|
|
7f6f777b47 | ||
|
|
68fb3b1a67 | ||
|
|
cd4a1d523b | ||
|
|
84cd6ba710 | ||
|
|
aa8492cd3d | ||
|
|
6f8710bd7a | ||
|
|
8b175b369a | ||
|
|
0be05967ca | ||
|
|
51f6165290 | ||
|
|
f147c83604 | ||
|
|
881de107c7 | ||
|
|
6b533dfbe1 | ||
|
|
2e3fb8beae | ||
|
|
e8baf5cd52 | ||
|
|
6c0289208f | ||
|
|
496f90fb56 | ||
|
|
696333e105 | ||
|
|
099a7aa972 | ||
|
|
b2aa568eb2 | ||
|
|
e685c8adcf | ||
|
|
19ddb670db | ||
|
|
8f1cd3e203 | ||
|
|
6b0fcd743f | ||
|
|
1d275936a4 | ||
|
|
f7c28a42c0 | ||
|
|
fa54829328 | ||
|
|
c8a7a3ac67 | ||
|
|
791b2608c5 | ||
|
|
ba8e40bc48 | ||
|
|
712626e791 | ||
|
|
734387a866 | ||
|
|
0988db9269 | ||
|
|
97d620b982 | ||
|
|
857d0a602f | ||
|
|
23f8aa8bdd | ||
|
|
03a04bd3fd | ||
|
|
57f91d046f | ||
|
|
758bc9b874 | ||
|
|
490a86796c | ||
|
|
f44270d913 | ||
|
|
4eed1b063e | ||
|
|
e3fe562d53 | ||
|
|
84ba81a5e5 | ||
|
|
d469f5da40 | ||
|
|
98e8de75dd | ||
|
|
e8d7985dba | ||
|
|
6aa680bf43 | ||
|
|
b629f07f5e | ||
|
|
9e4f571808 | ||
|
|
eaad9f3774 | ||
|
|
79f6359af1 | ||
|
|
ca5e9e03ab | ||
|
|
2ebdee2d42 | ||
|
|
c1773fb156 | ||
|
|
e05cc70f3a | ||
|
|
7d97ed5e93 | ||
|
|
823ee5462f | ||
|
|
64aebb5cf7 | ||
|
|
979713bb48 | ||
|
|
3b8efd628c | ||
|
|
f91033ef3d | ||
|
|
0f881a6de5 | ||
|
|
fe919f539e | ||
|
|
0b2efb006c | ||
|
|
7584af683e | ||
|
|
64358469ae | ||
|
|
28ad995982 | ||
|
|
3efb47fef1 | ||
|
|
1ca655f2f4 | ||
|
|
4fd6fc6e23 | ||
|
|
b6d21e7ef1 | ||
|
|
fb484b61da | ||
|
|
1ebfa3dd3f | ||
|
|
35bfb8679a | ||
|
|
dce407ca2b | ||
|
|
f9e90bee25 | ||
|
|
ea44096200 | ||
|
|
f3d19911c7 | ||
|
|
2214a4812c | ||
|
|
8b08afc1ea | ||
|
|
4d77723fe6 | ||
|
|
bd0364fcea | ||
|
|
3b466c0291 | ||
|
|
ade6faa6a3 | ||
|
|
c88959cc11 | ||
|
|
1e7fe81a60 | ||
|
|
726e3927f4 | ||
|
|
5f6b6d7ba0 | ||
|
|
62bc825bc8 | ||
|
|
de789ad8a9 | ||
|
|
e657acaee3 | ||
|
|
ba94f64ca0 | ||
|
|
26722781d2 | ||
|
|
2434df71aa | ||
|
|
f02d561d8b | ||
|
|
73c8715517 | ||
|
|
1a0a8cae67 | ||
|
|
a00665a3a1 | ||
|
|
126d514ee3 | ||
|
|
c0014f989f | ||
|
|
874413c3c2 | ||
|
|
bff2140b9f | ||
|
|
0aa20c7e8e | ||
|
|
8bf29b18da | ||
|
|
8330060bd9 | ||
|
|
b09d5a87fa | ||
|
|
b976976cda | ||
|
|
2a82d37da1 | ||
|
|
632f09382f | ||
|
|
558cc306e2 | ||
|
|
f1476befe2 | ||
|
|
cb386f0020 | ||
|
|
ef37fad7fc | ||
|
|
f42fa9515b | ||
|
|
fca2018279 | ||
|
|
1e0e755b0e | ||
|
|
757007ad7b | ||
|
|
bc2cc76b29 | ||
|
|
6cc997d4ee | ||
|
|
a2f24be087 | ||
|
|
dd38a66d2f | ||
|
|
c8c6927962 | ||
|
|
be7c4e3c8e | ||
|
|
c20849222c | ||
|
|
1e0cee46cd | ||
|
|
77f6a0ae89 | ||
|
|
b9bff03db1 | ||
|
|
74126b54ab | ||
|
|
48e81bc3bc | ||
|
|
0f98f0d1a3 | ||
|
|
494b20ba15 | ||
|
|
13886186c0 | ||
|
|
1611a8dded | ||
|
|
24fb41bb77 | ||
|
|
8977963b58 | ||
|
|
b5f50b1a5f | ||
|
|
0f52a0b6c6 | ||
|
|
9a783750b7 | ||
|
|
ec083fb951 | ||
|
|
e894dbb997 | ||
|
|
b36bc2523c | ||
|
|
0c0db36718 | ||
|
|
4343c52466 | ||
|
|
ea181dac0b | ||
|
|
09525daae8 | ||
|
|
874f9e8d6e | ||
|
|
822672c288 | ||
|
|
383586c5a4 | ||
|
|
b4d557534d | ||
|
|
e6921144fb | ||
|
|
f95509f4f4 | ||
|
|
5a0228d4fc | ||
|
|
5c96e4e44b | ||
|
|
4012bac31f | ||
|
|
4439c436fc | ||
|
|
b23119e44c | ||
|
|
e428d9cb5a | ||
|
|
6574db6f81 | ||
|
|
844d25483f | ||
|
|
2f6e7def42 | ||
|
|
dbf29c02a9 | ||
|
|
8303dd2d37 | ||
|
|
3e24a29e73 | ||
|
|
e3b32a7e92 | ||
|
|
20def41a6c | ||
|
|
aff3a367e6 | ||
|
|
80c94fec80 | ||
|
|
2d8d8178ba | ||
|
|
6ac1365e27 | ||
|
|
ef3938ca4a | ||
|
|
334a23c04d | ||
|
|
57be98ede3 | ||
|
|
52f97963f0 | ||
|
|
61a3f157ac | ||
|
|
bf4bf38dd2 | ||
|
|
4119656f69 | ||
|
|
3834e8d506 | ||
|
|
b4380d3a2f | ||
|
|
22ea0617e2 | ||
|
|
09c0994a34 | ||
|
|
0939e25da2 | ||
|
|
712e0a06e6 | ||
|
|
e73f745800 | ||
|
|
773ea797a1 | ||
|
|
aae20aeedf | ||
|
|
a3004e59fb | ||
|
|
5e76193f43 | ||
|
|
50a2627b55 | ||
|
|
cbddacb7b8 | ||
|
|
887e71b7b2 |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"append": [
|
||||
"assets/fox.glb.license",
|
||||
"assets/qwantani_afternoon_1k_hdr.jpg.license",
|
||||
"LICENSE"
|
||||
],
|
||||
"replace": {
|
||||
|
||||
26
.github/dependabot.yml
vendored
26
.github/dependabot.yml
vendored
@@ -1,26 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/example"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github/workflows/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "saturday"
|
||||
time: "09:00"
|
||||
14
.github/renovate.json5
vendored
Normal file
14
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":disableDependencyDashboard"
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "branch",
|
||||
"schedule": [ "* * * * 0,6" ],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [ "* * * * 0,6" ]
|
||||
}
|
||||
}
|
||||
35
.github/workflows/autoupdate.yml
vendored
35
.github/workflows/autoupdate.yml
vendored
@@ -1,35 +0,0 @@
|
||||
on: "pull_request_target"
|
||||
|
||||
permissions:
|
||||
pull-requests: "write"
|
||||
contents: "write"
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: "ubuntu-latest"
|
||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
||||
if: "${{ github.actor == 'dependabot[bot]' }}"
|
||||
steps:
|
||||
# This first step will fail if there's no metadata and so the approval
|
||||
# will not occur.
|
||||
- name: "Dependabot metadata"
|
||||
id: "dependabot-metadata"
|
||||
uses: "dependabot/fetch-metadata@v2"
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# Here the PR gets approved.
|
||||
- uses: "actions/checkout@v4"
|
||||
- name: "Approve a PR"
|
||||
run: "gh pr review --approve $PR_URL"
|
||||
env:
|
||||
PR_URL: "${{ github.event.pull_request.html_url }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# Finally, this sets the PR to allow auto-merging for patch and minor
|
||||
# updates if all checks pass
|
||||
- name: "Enable auto-merge for Dependabot PRs"
|
||||
#if: "${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}"
|
||||
run: "gh pr merge --auto --squash $PR_URL"
|
||||
env:
|
||||
PR_URL: "${{ github.event.pull_request.html_url }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -1,10 +1,6 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
@@ -42,10 +38,11 @@ jobs:
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
- run: "SKIP_BUILD_FRONTEND=true poetry install"
|
||||
- run: "SKIP_BUILD_FRONTEND=true poetry build"
|
||||
- run: "poetry lock"
|
||||
- run: "poetry install"
|
||||
- run: "poetry build" # Skips building frontend (not using task)
|
||||
|
||||
build-logo:
|
||||
name: "Build logo"
|
||||
@@ -57,9 +54,10 @@ jobs:
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
- run: "SKIP_BUILD_FRONTEND=true poetry install"
|
||||
- run: "poetry lock"
|
||||
- run: "poetry install"
|
||||
- run: "poetry run python yacv_server/logo.py"
|
||||
- uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
@@ -77,9 +75,10 @@ jobs:
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
- run: "SKIP_BUILD_FRONTEND=true poetry install"
|
||||
- run: "poetry lock"
|
||||
- run: "poetry install"
|
||||
- run: "YACV_DISABLE_SERVER=true poetry run python example/object.py"
|
||||
- uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
|
||||
2
.github/workflows/deploy1.yml
vendored
2
.github/workflows/deploy1.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
- run: "poetry version $CLEAN_VERSION"
|
||||
# Commit the changes and move the tag!
|
||||
|
||||
11
.github/workflows/deploy2.yml
vendored
11
.github/workflows/deploy2.yml
vendored
@@ -35,11 +35,7 @@ jobs:
|
||||
- uses: "actions/download-artifact@v4"
|
||||
with: # Downloads all artifacts from the build job
|
||||
path: "./public"
|
||||
- run: | # Merge the subdirectories of public into a single directory
|
||||
for dir in public/*; do
|
||||
mv "$dir/"* public/
|
||||
rmdir "$dir"
|
||||
done
|
||||
merge-multiple: true
|
||||
- uses: "actions/configure-pages@v5"
|
||||
- uses: "actions/upload-pages-artifact@v3"
|
||||
with:
|
||||
@@ -67,9 +63,10 @@ jobs:
|
||||
- run: "pipx install poetry"
|
||||
- uses: "actions/setup-python@v5"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.13"
|
||||
cache: "poetry"
|
||||
- run: "poetry install"
|
||||
- run: "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}"
|
||||
- run: "poetry publish --build"
|
||||
- run: "poetry run task build" # This task also builds the frontend (with reduced features for less size)
|
||||
- run: "poetry publish"
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -15,14 +15,24 @@ in a web browser.
|
||||
- Select any entity and measure bounding box size and distances.
|
||||
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
||||
- Fully-featured static deployment: just upload the viewer and models to your server.
|
||||
- Build123d playground! code and build your model fully inside the
|
||||
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code_url=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
|
||||
|
||||
## Usage
|
||||
|
||||
The [example](example) is a fully working project that shows how to use the viewer.
|
||||
|
||||
You can play with the latest
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||
(or
|
||||
[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)).
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
||||
|
||||

|
||||
|
||||
## Related projects
|
||||
|
||||
- [cq-studio](https://github.com/ccazabon/cq-studio) provides an alternative workflow that detects file changes instead
|
||||
of relying on an interactive environment like Jupyter for hot-reloading.
|
||||
Uses the same backend and frontend behind the scenes.
|
||||
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
|
||||
Yet Another CAD Viewer and other projects, with automatic updates.
|
||||
|
||||
@@ -3,9 +3,9 @@ https://www.npmjs.com/package/generate-license-file
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @google/model-viewer@3.5.0
|
||||
- @google/model-viewer@4.1.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -213,9 +213,9 @@ Apache License
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- source-map-js@1.2.0
|
||||
- source-map-js@1.2.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2009-2011, Mozilla Foundation and contributors
|
||||
All rights reserved.
|
||||
@@ -254,7 +254,7 @@ The following npm packages may be included in this product:
|
||||
- ndarray-ops@1.2.2
|
||||
- uniq@1.0.1
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -284,7 +284,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- ndarray@1.0.19
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -314,7 +314,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- promise-worker-transferable@1.0.4
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -522,9 +522,9 @@ Apache License
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- detect-libc@2.0.3
|
||||
- detect-libc@2.0.4
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -735,7 +735,7 @@ The following npm packages may be included in this product:
|
||||
- @types/ndarray@1.0.14
|
||||
- @types/trusted-types@2.0.7
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -761,11 +761,41 @@ MIT License
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- pako@2.1.0
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
|
||||
|
||||
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:
|
||||
|
||||
- lie@3.3.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
#Copyright (c) 2014-2018 Calvin Metcalf, Jordan Harband
|
||||
|
||||
@@ -777,11 +807,13 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- sharp@0.33.4
|
||||
- @img/sharp-linux-x64@0.33.5
|
||||
- @img/sharp-linuxmusl-x64@0.33.5
|
||||
- sharp@0.33.5
|
||||
|
||||
This package contains the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -979,9 +1011,9 @@ third-party archives.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- typescript@5.6.2
|
||||
- typescript@5.8.3
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Apache License
|
||||
|
||||
@@ -1043,11 +1075,11 @@ END OF TERMS AND CONDITIONS
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @lit/reactive-element@1.6.3
|
||||
- lit-element@3.3.3
|
||||
- lit@2.8.0
|
||||
- @lit/reactive-element@2.1.1
|
||||
- lit-element@4.2.1
|
||||
- lit@3.3.1
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
@@ -1082,9 +1114,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- lit-html@2.8.0
|
||||
- lit-html@3.3.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
@@ -1119,19 +1151,48 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @lit-labs/ssr-dom-shim@1.2.0
|
||||
- @lit-labs/ssr-dom-shim@1.4.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
BSD-3-Clause
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- ws@8.18.3
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
Copyright (c) 2013 Arnout Kazemier and contributors
|
||||
Copyright (c) 2016 Luigi Pinca and contributors
|
||||
|
||||
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:
|
||||
|
||||
- color-string@1.9.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2011 Heather Arthur <fayearthur@gmail.com>
|
||||
|
||||
@@ -1160,7 +1221,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- color-convert@2.0.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2011-2016 Heather Arthur <fayearthur@gmail.com>
|
||||
|
||||
@@ -1189,7 +1250,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- immediate@3.0.6
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, Domenic Denicola, Brian Cavalier
|
||||
|
||||
@@ -1218,7 +1279,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- color@4.2.3
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2012 Heather Arthur
|
||||
|
||||
@@ -1245,9 +1306,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @babel/parser@7.25.3
|
||||
- @babel/parser@7.28.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
||||
|
||||
@@ -1275,7 +1336,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- is-promise@2.2.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2014 Forbes Lindesay
|
||||
|
||||
@@ -1299,11 +1360,47 @@ THE SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- js-base64@3.7.7
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2014, Dan Kogai
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of {{{project}}} nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- estree-walker@2.0.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2015-20 [these people](https://github.com/Rich-Harris/estree-walker/graphs/contributors)
|
||||
|
||||
@@ -1319,7 +1416,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- csstype@3.1.3
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) 2017-2018 Fredrik Nicol
|
||||
|
||||
@@ -1347,7 +1444,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- entities@4.5.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright (c) Felix Böhm
|
||||
All rights reserved.
|
||||
@@ -1365,9 +1462,9 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- magic-string@0.30.11
|
||||
- magic-string@0.30.17
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright 2018 Rich Harris
|
||||
|
||||
@@ -1381,13 +1478,41 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- picocolors@1.0.1
|
||||
- @jridgewell/sourcemap-codec@1.5.4
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
Copyright 2024 Justin Ridgewell <justin@ridgewell.name>
|
||||
|
||||
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:
|
||||
|
||||
- picocolors@1.1.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
|
||||
Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -1403,32 +1528,24 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- to-fast-properties@2.0.0
|
||||
- @img/sharp-libvips-linux-x64@1.0.4
|
||||
- @img/sharp-libvips-linuxmusl-x64@1.0.4
|
||||
|
||||
This package contains the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014 Petka Antonov
|
||||
2015 Sindre Sorhus
|
||||
|
||||
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.
|
||||
LGPL-3.0-or-later
|
||||
|
||||
-----------
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @babel/helper-string-parser@7.24.8
|
||||
- @babel/helper-validator-identifier@7.24.7
|
||||
- @babel/types@7.25.2
|
||||
- @babel/helper-string-parser@7.27.1
|
||||
- @babel/helper-validator-identifier@7.27.1
|
||||
- @babel/types@7.28.2
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -1457,9 +1574,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- three-mesh-bvh@0.7.8
|
||||
- three-mesh-bvh@0.9.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -1489,7 +1606,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- three-orientation-gizmo@1.1.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -1517,9 +1634,129 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @monogrid/gainmap-js@3.0.3
|
||||
- state-local@1.0.7
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Suren Atoyan
|
||||
|
||||
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:
|
||||
|
||||
- vue-demi@0.14.10
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-present, Anthony Fu
|
||||
|
||||
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:
|
||||
|
||||
- @monaco-editor/loader@1.5.0
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Suren Atoyan
|
||||
|
||||
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:
|
||||
|
||||
- @guolao/vue-monaco-editor@1.5.5
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 guolao
|
||||
|
||||
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:
|
||||
|
||||
- @monogrid/gainmap-js@3.1.0
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -1549,7 +1786,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- @jamescoyle/vue-icon@0.1.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -1575,12 +1812,22 @@ SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- pyodide@0.28.0
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MPL-2.0
|
||||
|
||||
-----------
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @mdi/js@7.4.47
|
||||
- @mdi/svg@7.4.47
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
Pictogrammers Free License
|
||||
--------------------------
|
||||
@@ -1607,9 +1854,9 @@ The MIT license applies to all non-font and non-icon files.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- semver@7.6.2
|
||||
- semver@7.7.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The ISC License
|
||||
|
||||
@@ -1629,41 +1876,11 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @jridgewell/sourcemap-codec@1.5.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2015 Rich Harris
|
||||
|
||||
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:
|
||||
|
||||
- three@0.125.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License
|
||||
|
||||
@@ -1691,13 +1908,13 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- three@0.163.0
|
||||
- three@0.178.0
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2024 three.js authors
|
||||
Copyright © 2010-2025 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -1723,7 +1940,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- is-arrayish@0.3.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1753,7 +1970,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- simple-swizzle@0.2.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1781,13 +1998,43 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- vuetify@3.7.1
|
||||
- monaco-editor@0.52.2
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2023 John Jeremy Leider
|
||||
Copyright (c) 2016 - present Microsoft Corporation
|
||||
|
||||
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:
|
||||
|
||||
- vuetify@3.9.2
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-now Vuetify, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -1811,18 +2058,18 @@ THE SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @vue/compiler-core@3.5.5
|
||||
- @vue/compiler-dom@3.5.5
|
||||
- @vue/compiler-sfc@3.5.5
|
||||
- @vue/compiler-ssr@3.5.5
|
||||
- @vue/reactivity@3.5.5
|
||||
- @vue/runtime-core@3.5.5
|
||||
- @vue/runtime-dom@3.5.5
|
||||
- @vue/server-renderer@3.5.5
|
||||
- @vue/shared@3.5.5
|
||||
- vue@3.5.5
|
||||
- @vue/compiler-core@3.5.18
|
||||
- @vue/compiler-dom@3.5.18
|
||||
- @vue/compiler-sfc@3.5.18
|
||||
- @vue/compiler-ssr@3.5.18
|
||||
- @vue/reactivity@3.5.18
|
||||
- @vue/runtime-core@3.5.18
|
||||
- @vue/runtime-dom@3.5.18
|
||||
- @vue/server-renderer@3.5.18
|
||||
- @vue/shared@3.5.18
|
||||
- vue@3.5.18
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1850,9 +2097,9 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- ktx-parse@0.7.0
|
||||
- ktx-parse@1.0.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1882,9 +2129,9 @@ The following npm packages may be included in this product:
|
||||
|
||||
- ndarray-lanczos@0.3.0
|
||||
- ndarray-pixels@4.1.0
|
||||
- property-graph@2.0.0
|
||||
- property-graph@3.0.0
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1912,11 +2159,11 @@ SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @gltf-transform/core@4.0.8
|
||||
- @gltf-transform/extensions@4.0.8
|
||||
- @gltf-transform/functions@4.0.8
|
||||
- @gltf-transform/core@4.2.0
|
||||
- @gltf-transform/extensions@4.2.0
|
||||
- @gltf-transform/functions@4.2.0
|
||||
|
||||
These packages each contain the following license and notice below:
|
||||
These packages each contain the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1946,7 +2193,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- is-buffer@1.1.6
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1974,9 +2221,9 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- postcss@8.4.45
|
||||
- postcss@8.5.6
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -2003,9 +2250,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- nanoid@3.3.7
|
||||
- nanoid@3.3.11
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -2034,7 +2281,7 @@ The following npm package may be included in this product:
|
||||
|
||||
- color-name@1.1.4
|
||||
|
||||
This package contains the following license and notice below:
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015 Dmitry Ivanov
|
||||
@@ -2055,6 +2302,11 @@ glTF conversion by @AsoboStudio and @scurest
|
||||
|
||||
-----------
|
||||
|
||||
CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing)
|
||||
https://polyhaven.com/a/qwantani_afternoon
|
||||
|
||||
-----------
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Yeicor
|
||||
|
||||
BIN
assets/qwantani_afternoon_1k_hdr.jpg
Normal file
BIN
assets/qwantani_afternoon_1k_hdr.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
2
assets/qwantani_afternoon_1k_hdr.jpg.license
Normal file
2
assets/qwantani_afternoon_1k_hdr.jpg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing)
|
||||
https://polyhaven.com/a/qwantani_afternoon
|
||||
9
build.py
9
build.py
@@ -1,9 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Building the frontend is optional
|
||||
if os.getenv('SKIP_BUILD_FRONTEND') is None and os.path.exists('package.json'):
|
||||
# When building the backend, make sure the frontend is built first
|
||||
subprocess.run(['yarn', 'install'], check=True)
|
||||
subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], check=True)
|
||||
@@ -36,5 +36,5 @@ Once you have the `object.glb` file, you can host it on any static file server a
|
||||
|
||||
For the example model, the build process is set up in [build.yml](../.github/workflows/build.yml), the upload process
|
||||
is set up in [deploy.yml](../.github/workflows/deploy.yml), and the final link is:
|
||||
https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=example.glb
|
||||
https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=example.glb&preload=example_hl.glb
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
|
||||
from build123d import * # Also works with cadquery objects!
|
||||
from build123d import Compound
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
@@ -15,9 +16,17 @@ with BuildPart() as example:
|
||||
Box(10, 10, 5)
|
||||
Cylinder(4, 5, mode=Mode.SUBTRACT)
|
||||
|
||||
# Show it in the frontend with hot-reloading
|
||||
show(example)
|
||||
# Custom colors (optional)
|
||||
example.color = (0.1, 0.3, 0.1, 1) # RGBA
|
||||
to_highlight = example.edges().group_by(Axis.Z)[-1]
|
||||
example_hl = Compound(to_highlight).translate((0, 0, 1e-3)) # To avoid z-fighting
|
||||
example_hl.color = (1, 1, .0, 1)
|
||||
|
||||
# Show it in the frontend with hot-reloading (texture and other keyword arguments are optional)
|
||||
texture = ( # MIT License Framework7 Line Icons: https://www.svgrepo.com/svg/437552/checkmark-seal
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAQAAAAB+tbP6AAAAQ0lEQVQI12P4b3+A4Z/8AYYHBw8w"
|
||||
"HHxwgOH8HyD+AsRPDjDMP+fAYD+fgcESiGfYOTCcqTnAcK4GogakFqQHpBdoBgAbGiPSbdzkhgAAAABJRU5ErkJggg==")
|
||||
show(example, example_hl, texture=texture)
|
||||
|
||||
# %%
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./mis
|
||||
import {SceneMgr} from "./misc/scene";
|
||||
import {Document} from "@gltf-transform/core";
|
||||
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
|
||||
import {mdiPlus} from '@mdi/js'
|
||||
import {mdiCube, mdiPlus, mdiScriptTextPlay} from '@mdi/js'
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
|
||||
// NOTE: The ModelViewer library is big (THREE.js), so we split it and import it asynchronously
|
||||
@@ -21,7 +22,7 @@ const ModelViewerWrapper = defineAsyncComponent({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
|
||||
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > window.innerHeight);
|
||||
|
||||
const sceneUrl = ref("")
|
||||
const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
||||
@@ -52,10 +53,11 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
let model = event.models[modelIndex];
|
||||
tools.value?.removeObjectSelections(model.name);
|
||||
try {
|
||||
let loadHelpers = (await settings).loadHelpers;
|
||||
if (!model.isRemove) {
|
||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
|
||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && loadHelpers, isLast);
|
||||
} else {
|
||||
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
|
||||
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && loadHelpers, isLast);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error loading model", model, e);
|
||||
@@ -79,21 +81,50 @@ 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(""));
|
||||
}
|
||||
});
|
||||
let preloadingModels = ref<Array<string>>([]);
|
||||
(async () => { // Start loading all configured models ASAP
|
||||
let sett = await settings;
|
||||
if (sett.preload.length > 0) {
|
||||
watch(viewer, (newViewer) => {
|
||||
if (newViewer) {
|
||||
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
|
||||
' models from:</tspan>' + sett.preload.map((url: string) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
|
||||
}
|
||||
});
|
||||
for (let model of sett.preload) {
|
||||
preloadingModels.value.push(model);
|
||||
let removeFromPreloadingModels = () => {
|
||||
preloadingModels.value = preloadingModels.value.filter((m) => m !== model);
|
||||
};
|
||||
networkMgr.load(model).then(removeFromPreloadingModels).catch((e) => {
|
||||
removeFromPreloadingModels()
|
||||
console.error("Error preloading model", model, e);
|
||||
});
|
||||
}
|
||||
} // else No preloaded models (useful for playground mode)
|
||||
})();
|
||||
|
||||
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:");
|
||||
if (modelUrl) await networkMgr.load(modelUrl);
|
||||
}
|
||||
|
||||
// Detect dropped .glb files and load them manually
|
||||
document.body.addEventListener("dragover", e => {
|
||||
e.preventDefault(); // Allow drop
|
||||
});
|
||||
|
||||
document.body.addEventListener("drop", async e => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
if (ext === 'glb' || ext === 'gltf') {
|
||||
await networkMgr.load(file);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,7 +132,28 @@ async function loadModelManual() {
|
||||
|
||||
<!-- The main content of the app is the model-viewer with the SVG "2D" overlay -->
|
||||
<v-main id="main">
|
||||
<model-viewer-wrapper ref="viewer" :src="sceneUrl"/>
|
||||
<model-viewer-wrapper v-if="sceneDocument.getRoot().listMeshes().length > 0" ref="viewer" :src="sceneUrl"/>
|
||||
<!-- A nice no model loaded alternative to avoid breaking model-viewer-wrapper -->
|
||||
<div v-else style="height: 100%; overflow-y: auto">
|
||||
<v-toolbar-title class="text-center ma-16 text-h5">No model loaded</v-toolbar-title>
|
||||
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiScriptTextPlay" type="mdi"/> Open playground...
|
||||
</v-btn>
|
||||
<v-btn @click="networkMgr.load('https://yeicor-3d.github.io/yet-another-cad-viewer/logo.glb')"
|
||||
class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo model...
|
||||
</v-btn>
|
||||
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiPlus" type="mdi"/> Load model manually...
|
||||
</v-btn>
|
||||
<span v-if="preloadingModels.length > 0" class="d-block text-center my-16">
|
||||
<span class="d-block text-center text-h6">Still trying to load the following:</span>
|
||||
<span class="d-block text-center" v-for="(model, index) in preloadingModels" :key="index">
|
||||
{{ model }}<span v-if="index < preloadingModels.length - 1">, </span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</v-main>
|
||||
|
||||
<!-- The left collapsible sidebar has the list of models -->
|
||||
@@ -114,7 +166,7 @@ async function loadModelManual() {
|
||||
<svg-icon :path="mdiPlus" type="mdi"/>
|
||||
</v-btn>
|
||||
</template>
|
||||
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
|
||||
<models ref="models" :viewer="viewer" @remove-model="onModelRemoveRequest"/>
|
||||
</sidebar>
|
||||
|
||||
<!-- The right collapsible sidebar has the list of tools -->
|
||||
@@ -122,7 +174,7 @@ async function loadModelManual() {
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Tools</v-toolbar-title>
|
||||
</template>
|
||||
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
<tools ref="tools" :viewer="viewer" @find-model="models?.findModel" @update-model="onModelUpdateRequest"/>
|
||||
</sidebar>
|
||||
|
||||
</v-layout>
|
||||
@@ -132,6 +184,6 @@ async function loadModelManual() {
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
31
frontend/misc/IfNotSmallBuild.vue
Normal file
31
frontend/misc/IfNotSmallBuild.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import {mdiLockQuestion} from "@mdi/js";
|
||||
import {VBtn, VTooltip} from "vuetify/lib/components/index.mjs";
|
||||
// @ts-expect-error
|
||||
import SvgIcon from "@jamescoyle/vue-icon";
|
||||
|
||||
// @ts-expect-error
|
||||
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
|
||||
|
||||
function clickedButton() { // Redirect to the main build
|
||||
window.open("https://yeicor-3d.github.io/yet-another-cad-viewer/" + window.location.search + window.location.hash, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- @ts-ignore-->
|
||||
<!-- Include the children as this is a full build -->
|
||||
<slot v-if="!isSmallBuild"/>
|
||||
<!-- A small info button saying that a feature is missing, and linking to the main build -->
|
||||
<v-btn v-else icon @click="clickedButton" base-color="#a00" style="margin: auto; display: block;">
|
||||
<v-tooltip activator="parent">
|
||||
This feature is not available in the small build.<br/>
|
||||
Click to go to the main build.
|
||||
</v-tooltip>
|
||||
<svg-icon :path="mdiLockQuestion" type="mdi"/>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import {ref} from "vue";
|
||||
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
|
||||
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
|
||||
import {unpartition, mergeDocuments} from "@gltf-transform/functions";
|
||||
import {mergeDocuments, unpartition} from "@gltf-transform/functions";
|
||||
|
||||
let io = new WebIO();
|
||||
export let extrasNameKey = "__yacv_name";
|
||||
export let extrasNameValueHelpers = "__helpers";
|
||||
|
||||
// @ts-expect-error
|
||||
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
|
||||
|
||||
/**
|
||||
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
|
||||
*
|
||||
@@ -12,11 +15,11 @@ 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, networkFinished: () => void = () => {
|
||||
export async function mergePartial(url: string | Blob, name: string, document: Document, networkFinished: () => void = () => {
|
||||
}): Promise<Document> {
|
||||
// Fetch the complete document from the network
|
||||
// This could be done at the same time as the document is being processed, but I wanted better metrics
|
||||
let response = await fetch(url);
|
||||
let response = await fetchOrRead(url);
|
||||
let buffer = await response.arrayBuffer();
|
||||
networkFinished();
|
||||
|
||||
@@ -27,18 +30,20 @@ export async function mergePartial(url: string, name: string, document: Document
|
||||
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 (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
|
||||
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
|
||||
// @ts-expect-error
|
||||
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||
// @ts-expect-error
|
||||
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||
io.registerExtensions([KHRDracoMeshCompression])
|
||||
.registerDependencies({
|
||||
'draco3d.decoder': await dracoDecoderWeb.default({}),
|
||||
'draco3d.encoder': await dracoEncoderWeb.default({})
|
||||
});
|
||||
} else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||
} else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
|
||||
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
|
||||
io.registerExtensions([EXTTextureWebP]);
|
||||
@@ -113,3 +118,30 @@ function mergeScenes(): Transform {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetches a URL or reads it if it is a Blob URL */
|
||||
async function fetchOrRead(url: string | Blob) {
|
||||
if (url instanceof Blob) {
|
||||
// Use the FileReader API as fetch does not support Blob URLs
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = (event: ProgressEvent<FileReader>) => {
|
||||
if (event.target && event.target.result) {
|
||||
resolve(new Response(event.target.result));
|
||||
} else {
|
||||
reject(new Error("Failed to read Blob URL: " + url));
|
||||
}
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
reject(new Error("Error reading Blob URL: " + url + " - " + error));
|
||||
};
|
||||
// Read the Blob URL as an ArrayBuffer
|
||||
reader.readAsArrayBuffer(new Blob([url]));
|
||||
});
|
||||
} else {
|
||||
// Fetch the URL
|
||||
return fetch(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ 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 async function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
|
||||
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
|
||||
// Create transformed positions for the inner faces of the box
|
||||
let allPositions: number[] = [];
|
||||
let allIndices: number[] = [];
|
||||
|
||||
@@ -4,12 +4,12 @@ const batchTimeout = 250; // ms
|
||||
|
||||
export class NetworkUpdateEventModel {
|
||||
name: string;
|
||||
url: string;
|
||||
url: string | Blob;
|
||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||
hash: string | null;
|
||||
isRemove: boolean | null; // This is null for a shutdown event
|
||||
|
||||
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) {
|
||||
constructor(name: string, url: string | Blob, hash: string | null, isRemove: boolean | null) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.hash = hash;
|
||||
@@ -42,18 +42,26 @@ export class NetworkManager extends EventTarget {
|
||||
*
|
||||
* Updates will be emitted as "update" events, including the download URL and the model name.
|
||||
*/
|
||||
async load(url: string) {
|
||||
if (url.startsWith("dev+")) {
|
||||
async load(url: string | Blob) {
|
||||
if (!(url instanceof Blob) && (url.startsWith("dev+") || url.startsWith("dev "))) {
|
||||
let baseUrl = new URL(url.slice(4));
|
||||
baseUrl.searchParams.set("api_updates", "true");
|
||||
await this.monitorDevServer(baseUrl);
|
||||
} else {
|
||||
// Get the last part of the URL as the "name" of the model
|
||||
let name = url.split("/").pop();
|
||||
name = name?.split(".")[0] || `unknown-${Math.random()}`;
|
||||
// Use a head request to get the hash of the file
|
||||
let response = await fetch(url, {method: "HEAD"});
|
||||
let hash = response.headers.get("etag");
|
||||
let name;
|
||||
let hash = null;
|
||||
if (url instanceof Blob) {
|
||||
if (url instanceof File) name = (url as File).name
|
||||
else name = `blob-${Math.random()}`;
|
||||
name = name.replace('.glb', '').replace('.gltf', '');
|
||||
} else {
|
||||
// Get the last part of the URL as the "name" of the model
|
||||
name = url.split("/").pop();
|
||||
name = name?.split(".")[0] || `unknown-${Math.random()}`;
|
||||
// Use a head request to get the hash of the file
|
||||
let response = await fetch(url, {method: "HEAD"});
|
||||
hash = response.headers.get("etag");
|
||||
}
|
||||
// Only trigger an update if the hash has changed
|
||||
this.foundModel(name, hash, url, false);
|
||||
}
|
||||
@@ -61,6 +69,7 @@ export class NetworkManager extends EventTarget {
|
||||
|
||||
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
|
||||
while (!stop()) {
|
||||
let monitorEveryMs = (await settings).monitorEveryMs;
|
||||
try {
|
||||
// WARNING: This will spam the console logs with failed requests when the server is down
|
||||
const controller = new AbortController();
|
||||
@@ -82,16 +91,16 @@ export class NetworkManager extends EventTarget {
|
||||
}
|
||||
} else {
|
||||
// Server is down, wait a little longer before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
|
||||
await new Promise(resolve => setTimeout(resolve, 10 * monitorEveryMs));
|
||||
}
|
||||
controller.abort();
|
||||
} catch (e) { // Ignore errors (retry very soon)
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
|
||||
await new Promise(resolve => setTimeout(resolve, monitorEveryMs));
|
||||
}
|
||||
}
|
||||
|
||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
||||
private foundModel(name: string, hash: string | null, url: string | Blob, isRemove: boolean | null, disconnect: () => void = () => {
|
||||
}) {
|
||||
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {type Ref} from 'vue';
|
||||
import {Document} from '@gltf-transform/core';
|
||||
import {Buffer, Document, Scene} from '@gltf-transform/core';
|
||||
import {extrasNameKey, extrasNameValueHelpers, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
||||
import {newAxes, newGridBox} from "./helpers";
|
||||
import {Vector3} from "three/src/math/Vector3.js"
|
||||
@@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js"
|
||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||
export class SceneMgr {
|
||||
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
|
||||
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
||||
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string | Blob, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
||||
let loadStart = performance.now();
|
||||
let loadNetworkEnd: number;
|
||||
|
||||
@@ -80,7 +80,19 @@ export class SceneMgr {
|
||||
|
||||
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> {
|
||||
let bb = SceneMgr.getBoundingBox(document);
|
||||
if (!bb) return document;
|
||||
if (!bb) return document; // Empty document, no helpers to show
|
||||
|
||||
// If only the helpers remain, go back to the empty scene
|
||||
let noOtherModels = true;
|
||||
for (let elem of document.getGraph().listEdges().map(e => e.getChild())) {
|
||||
if (elem.getExtras() && !(elem instanceof Scene) && !(elem instanceof Buffer) &&
|
||||
elem.getExtras()[extrasNameKey] !== extrasNameValueHelpers) {
|
||||
// There are other elements in the document, so we can show the helpers
|
||||
noOtherModels = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (noOtherModels) return await removeModel(extrasNameValueHelpers, document);
|
||||
|
||||
// Create the helper axes and grid box
|
||||
let helpersDoc = new Document();
|
||||
@@ -88,7 +100,9 @@ export class SceneMgr {
|
||||
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);
|
||||
let newDocument = await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
|
||||
URL.revokeObjectURL(helpersUrl);
|
||||
return newDocument;
|
||||
}
|
||||
|
||||
/** Serializes the current document into a GLB and updates the viewerSrc */
|
||||
@@ -100,6 +114,7 @@ export class SceneMgr {
|
||||
let buffer = await toBuffer(document);
|
||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||
if (sceneUrl.value.startsWith("blob:")) URL.revokeObjectURL(sceneUrl.value);
|
||||
sceneUrl.value = URL.createObjectURL(blob);
|
||||
|
||||
return document;
|
||||
|
||||
@@ -1,35 +1,125 @@
|
||||
// These are the default values for the settings, which are overridden below
|
||||
export const settings = {
|
||||
preload: [
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/fox.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
"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, // Global animation toggle
|
||||
arModes: 'webxr scene-viewer quick-look',
|
||||
zoomSensitivity: 0.25,
|
||||
orbitSensitivity: 1,
|
||||
panSensitivity: 1,
|
||||
exposure: 1,
|
||||
shadowIntensity: 0,
|
||||
background: '',
|
||||
}
|
||||
import {ungzip} from "pako";
|
||||
import {b64UrlDecode} from "../tools/b64.ts";
|
||||
|
||||
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
|
||||
function parseSetting(name: string, value: string): any {
|
||||
export const settings = (async () => {
|
||||
let settings = {
|
||||
preload: [
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/fox.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
'<auto>', // Get the default preload URL if not overridden
|
||||
],
|
||||
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, // Global animation toggle
|
||||
arModes: 'webxr scene-viewer quick-look',
|
||||
zoomSensitivity: 0.25,
|
||||
orbitSensitivity: 1,
|
||||
panSensitivity: 1,
|
||||
exposure: 1,
|
||||
shadowIntensity: 0,
|
||||
// Nice low-res outdoor/high-contrast HDRI image (CC0 licensed) for lighting
|
||||
environment: new URL('../../assets/qwantani_afternoon_1k_hdr.jpg', import.meta.url).href,
|
||||
environmentIntensity: 1.0,
|
||||
// Uniform (1x1 pixel) medium gray background for visibility (following dark/light mode)
|
||||
skybox: (window.matchMedia("(prefers-color-scheme: dark)").matches ?
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEU4ODiyn42XAAAACklEQVQI" +
|
||||
"12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==" :
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEW6urpaLVq8AAAACklEQVQI" +
|
||||
"12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="),
|
||||
|
||||
// Playground settings
|
||||
pg_code: "", // Automatically loaded and executed code for the playground
|
||||
pg_code_url: "", // URL to load the code from (overrides pg_code)
|
||||
pg_opacity_loading: -1, // Opacity of the code during first load and run (< 0 is 0.0 if preload and 0.9 if not)
|
||||
pg_opacity_loaded: 0.9, // Opacity of the code after it has been run for the first time
|
||||
};
|
||||
|
||||
// Auto-override any settings from the URL (either GET parameters or hash)
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key in settings) (settings as any)[key] = parseSetting(key, value, settings);
|
||||
})
|
||||
if (url.hash.length > 0) { // Hash has bigger limits as it is not sent to the server
|
||||
const hash = url.hash.slice(1);
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
hashParams.forEach((value, key) => {
|
||||
if (key in settings) (settings as any)[key] = parseSetting(key, value, settings);
|
||||
});
|
||||
}
|
||||
|
||||
// Grab the code from the URL if it is set
|
||||
if (settings.pg_code_url.length > 0) {
|
||||
// If the code URL is set, override the code
|
||||
try {
|
||||
const response = await fetch(settings.pg_code_url);
|
||||
if (response.ok) {
|
||||
settings.pg_code = await response.text();
|
||||
} else {
|
||||
console.warn("Failed to load code from URL:", settings.pg_code_url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching code from URL:", settings.pg_code_url, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
|
||||
for (let i = 0; i < settings.preload.length; i++) {
|
||||
let url = settings.preload[i];
|
||||
// Ignore empty preload URLs to allow overriding default auto behavior
|
||||
if (url === '') {
|
||||
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
|
||||
continue; // Skip this preload URL
|
||||
}
|
||||
// Handle special <auto> preload URL
|
||||
if (url === '<auto>') {
|
||||
if (settings.pg_code != "") { // <auto> means no preload URL if code is set
|
||||
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
|
||||
continue; // Skip this preload URL
|
||||
}
|
||||
const possibleBackend = new URL("./?api_updates=true", window.location.href)
|
||||
await fetch(possibleBackend, {method: "HEAD"}).then((response) => {
|
||||
if (response.ok && response.headers.get("Content-Type") === "text/event-stream") {
|
||||
// Frontend served by the backend: default to this URL for updates
|
||||
url = "dev+" + possibleBackend.href;
|
||||
}
|
||||
}).catch((error) => console.error("Failed to check for backend:", error));
|
||||
if (url === '<auto>') { // Fallback to the default preload URL of localhost
|
||||
url = "dev+http://localhost:32323";
|
||||
}
|
||||
}
|
||||
settings.preload[i] = url;
|
||||
}
|
||||
|
||||
// Auto-decompress the code and other playground settings
|
||||
if (settings.pg_code.length > 0) {
|
||||
try {
|
||||
settings.pg_code = ungzip(b64UrlDecode(settings.pg_code), {to: 'string'});
|
||||
} catch (error) {
|
||||
console.log("pg_code is not base64url+gzipped, assuming raw code. Decoding error:", error);
|
||||
}
|
||||
if (settings.pg_opacity_loading < 0) {
|
||||
// If the opacity is not set, use 0.0 if preload is set, otherwise 0.9
|
||||
settings.pg_opacity_loading = settings.preload.length > 0 ? 0.0 : 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
})()
|
||||
|
||||
function parseSetting(name: string, value: string, settings: any): any {
|
||||
let arrayElem = name.endsWith(".0")
|
||||
if (arrayElem) name = name.slice(0, -2);
|
||||
let prevValue = (settings as any)[name];
|
||||
@@ -42,7 +132,7 @@ function parseSetting(name: string, value: string): any {
|
||||
} else {
|
||||
toExtend = prevValue;
|
||||
}
|
||||
toExtend.push(parseSetting(name + ".0", value));
|
||||
toExtend.push(parseSetting(name + ".0", value, settings));
|
||||
return toExtend;
|
||||
} else {
|
||||
prevValue = prevValue[0];
|
||||
@@ -59,9 +149,3 @@ function parseSetting(name: string, value: string): any {
|
||||
throw new Error(`Unknown setting type: ${typeof prevValue} -- ${prevValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-override any settings from the URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key in settings) (settings as any)[key] = parseSetting(key, value);
|
||||
})
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
mdiVectorLine,
|
||||
mdiVectorRectangle
|
||||
} from '@mdi/js'
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import {BackSide, FrontSide} from "three/src/constants.js";
|
||||
import {Box3} from "three/src/math/Box3.js";
|
||||
@@ -34,6 +35,8 @@ 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"
|
||||
import {currentSceneRotation} from "../viewer/lighting.ts";
|
||||
import {Matrix4} from "three/src/math/Matrix4.js";
|
||||
|
||||
const props = defineProps<{
|
||||
meshes: Array<Mesh>,
|
||||
@@ -55,7 +58,11 @@ const clipPlaneY = ref(1);
|
||||
const clipPlaneSwappedY = ref(false);
|
||||
const clipPlaneZ = ref(1);
|
||||
const clipPlaneSwappedZ = ref(false);
|
||||
const edgeWidth = ref(settings.edgeWidth);
|
||||
const edgeWidth = ref(0);
|
||||
(async () => {
|
||||
let s = await settings;
|
||||
edgeWidth.value = s.edgeWidth;
|
||||
})();
|
||||
|
||||
// Misc properties
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
@@ -111,7 +118,8 @@ function onWireframeChange(newWireframe: boolean) {
|
||||
if (!scene || !sceneModel) return;
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material && child.material.wireframe !== newWireframe) {
|
||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
||||
if (child.material && child.material.wireframe !== newWireframe && childIsFace) {
|
||||
child.material.wireframe = newWireframe;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
@@ -149,10 +157,11 @@ function onClipPlanesChange() {
|
||||
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x);
|
||||
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 rotSceneMatrix = new Matrix4().makeRotationY(currentSceneRotation);
|
||||
let planes = [
|
||||
new Plane(new Vector3(-1, 0, 0), offsetX),
|
||||
new Plane(new Vector3(0, -1, 0), offsetY),
|
||||
new Plane(new Vector3(0, 0, 1), -offsetZ),
|
||||
new Plane(new Vector3(-1, 0, 0), offsetX).applyMatrix4(rotSceneMatrix),
|
||||
new Plane(new Vector3(0, -1, 0), offsetY).applyMatrix4(rotSceneMatrix),
|
||||
new Plane(new Vector3(0, 0, 1), -offsetZ).applyMatrix4(rotSceneMatrix),
|
||||
];
|
||||
if (clipPlaneSwappedX.value) planes[0].negate();
|
||||
if (clipPlaneSwappedY.value) planes[1].negate();
|
||||
@@ -223,9 +232,10 @@ function onEdgeWidthChange(newEdgeWidth: number) {
|
||||
line.userData.niceLine = line2;
|
||||
// line.parent!.remove(line); // Keep it for better raycast and selection!
|
||||
line2.userData.noHit = true;
|
||||
line2.visible = enabledFeatures.value.includes(1);
|
||||
edgeWidthChangeCleanup.push(() => {
|
||||
line2.parent!.remove(line2);
|
||||
line.visible = true;
|
||||
line.visible = enabledFeatures.value.includes(1);
|
||||
props.viewer!!.onElemReady((elem) => {
|
||||
elem.removeEventListener('resize', () => resizeListener(elem));
|
||||
});
|
||||
@@ -327,7 +337,7 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
|
||||
<template>
|
||||
<v-expansion-panel :value="modelName">
|
||||
<v-expansion-panel-title collapse-icon="hide-this-icon" expand-icon="hide-this-icon">
|
||||
<v-expansion-panel-title>
|
||||
<v-btn-toggle v-model="enabledFeatures" color="surface-light" multiple @click.stop>
|
||||
<v-btn icon>
|
||||
<v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip>
|
||||
@@ -426,10 +436,6 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
}
|
||||
|
||||
/* More compact accordions */
|
||||
.v-expansion-panel {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.v-expansion-panel-title {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -440,11 +446,11 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
--v-btn-height: 16px;
|
||||
--v-btn-height: 12px;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
width: 179px;
|
||||
width: 172px;
|
||||
font-size: 110%;
|
||||
overflow-x: clip;
|
||||
overflow-y: visible; /* HACK: bottom of text is lost otherwise (due to buggy -webkit-box bounds?) */
|
||||
@@ -461,10 +467,6 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.hide-this-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mdi-checkbox-blank-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,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z"/></svg>');
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import Model from "./Model.vue";
|
||||
import {inject, ref, type Ref} from "vue";
|
||||
|
||||
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
|
||||
const emit = defineEmits<{ remove: [string] }>()
|
||||
const emit = defineEmits<{ removeModel: [string] }>()
|
||||
|
||||
let {sceneDocument} = inject<{ sceneDocument: Ref<Document> }>('sceneDocument')!!;
|
||||
|
||||
let expandedNames = ref<Array<string>>([]);
|
||||
const expandedNames = ref<Array<string>>([]);
|
||||
|
||||
function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
|
||||
// Grouped by shared name
|
||||
@@ -32,7 +32,7 @@ function meshName(mesh: Mesh) {
|
||||
}
|
||||
|
||||
function onRemove(mesh: Mesh) {
|
||||
emit('remove', meshName(mesh))
|
||||
emit('removeModel', meshName(mesh))
|
||||
}
|
||||
|
||||
function findModel(name: string) {
|
||||
@@ -44,7 +44,7 @@ defineExpose({findModel})
|
||||
|
||||
<template>
|
||||
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
|
||||
v-model="expandedNames" multiple>
|
||||
v-model="expandedNames as any" multiple>
|
||||
<model :meshes="meshes" :viewer="props.viewer" @remove="onRemove(meshes[0])"/>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
@@ -61,4 +61,4 @@ defineExpose({findModel})
|
||||
.v-overlay--active > .v-overlay__content {
|
||||
display: block !important; /* HACK: Fix buggy tooltips not showing? */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
5
frontend/shims.d.ts
vendored
5
frontend/shims.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
// 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'
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, onUpdated, ref} from "vue";
|
||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
// @ts-expect-error
|
||||
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {currentSceneRotation} from "../viewer/lighting.ts";
|
||||
|
||||
// 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";
|
||||
import {Euler} from "three/src/math/Euler.js";
|
||||
|
||||
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
|
||||
|
||||
@@ -48,7 +51,7 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen
|
||||
axis.direction.y = -axis.direction.y;
|
||||
axis.direction.z = -axis.direction.z;
|
||||
}
|
||||
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z);
|
||||
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z) + currentSceneRotation;
|
||||
wantedPhi = Math.asin(-axis.direction.y) + Math.PI / 2;
|
||||
attempt++;
|
||||
}
|
||||
@@ -66,7 +69,12 @@ let gizmo: HTMLElement & { update: () => void }
|
||||
|
||||
function updateGizmo() {
|
||||
if (gizmo.isConnected) {
|
||||
// HACK: Update camera temporarily to match skybox rotation before updating the gizmo and go back
|
||||
let prevRot = ((gizmo as any).camera).rotation.clone() as Euler;
|
||||
let thetaMat = new Matrix4().makeRotationY(-currentSceneRotation);
|
||||
((gizmo as any).camera).rotation.setFromRotationMatrix(thetaMat.multiply(new Matrix4().makeRotationFromEuler(prevRot)));
|
||||
gizmo.update();
|
||||
((gizmo as any).camera).rotation.set(prevRot.x, prevRot.y, prevRot.z);
|
||||
requestIdleCallback(updateGizmo, {timeout: 250});
|
||||
}
|
||||
}
|
||||
|
||||
359
frontend/tools/PlaygroundDialogContent.vue
Normal file
359
frontend/tools/PlaygroundDialogContent.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<script setup lang="ts">
|
||||
import {setupMonaco} from "./monaco.ts";
|
||||
import {VueMonacoEditor} from '@guolao/vue-monaco-editor'
|
||||
import {nextTick, onMounted, ref, shallowRef} from "vue";
|
||||
import Loading from "../misc/Loading.vue";
|
||||
import {newPyodideWorker} from "./pyodide-worker-api.ts";
|
||||
import {
|
||||
mdiBroom,
|
||||
mdiCircleOpacity,
|
||||
mdiClose,
|
||||
mdiContentSave,
|
||||
mdiFolderOpen,
|
||||
mdiPlay,
|
||||
mdiReload,
|
||||
mdiShare
|
||||
} from "@mdi/js";
|
||||
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import {version as pyodideVersion} from "pyodide";
|
||||
import {gzip} from 'pako';
|
||||
import {b64UrlEncode} from "./b64.ts";
|
||||
import {Base64} from 'js-base64'; // More compatible with binary data from python...
|
||||
import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
|
||||
import {settings} from "../misc/settings.ts";
|
||||
// @ts-expect-error
|
||||
import playgroundStartupCode from './PlaygroundStartup.py?raw';
|
||||
|
||||
const model = defineModel<{ code: string, firstTime: boolean }>({required: true}); // Initial code should only be set on first load!
|
||||
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
|
||||
|
||||
// ============ LOAD MONACO EDITOR ============
|
||||
setupMonaco() // Must be called before using the editor
|
||||
|
||||
const outputText = ref(``);
|
||||
|
||||
function output(text: string) {
|
||||
outputText.value += text; // Append to output
|
||||
// Avoid too much output, keep it reasonable
|
||||
let max_output = 10000; // 10k characters
|
||||
if (outputText.value.length > max_output) {
|
||||
outputText.value = outputText.value.slice(-max_output); // Keep only the last 10k characters
|
||||
}
|
||||
console.log(text); // Also log to console
|
||||
nextTick(() => { // Scroll to bottom
|
||||
const consoleElement = document.querySelector('.playground-console');
|
||||
if (consoleElement) {
|
||||
consoleElement.scrollTop = consoleElement.scrollHeight;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const MONACO_EDITOR_OPTIONS = {
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true,
|
||||
}
|
||||
|
||||
const editorTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? `vs-dark` : `vs`
|
||||
const editor = shallowRef()
|
||||
const handleMount = (editorInstance: typeof VueMonacoEditor) => (editor.value = editorInstance)
|
||||
const opacity = ref(0.9); // Opacity for the editor (overriden when settings are loaded)
|
||||
|
||||
// ============ LOAD PYODIDE (ASYNC) ============
|
||||
let pyodideWorker: ReturnType<typeof newPyodideWorker> | null = (import.meta as any).hot?.data?.pyodideWorker || null;
|
||||
const running = ref(true);
|
||||
|
||||
async function setupPyodide(first: boolean, loadSnapshot: Uint8Array | undefined = undefined) {
|
||||
running.value = true;
|
||||
if (opacity.value == 0.0 && !first) opacity.value = 0.9; // User doesn't know how to show code again, reset after reopening
|
||||
if (pyodideWorker === null) {
|
||||
output("Creating new Pyodide worker...\n");
|
||||
pyodideWorker = newPyodideWorker(Object.assign({
|
||||
// Note: python wheels are downloaded from the CDN, as we can't know which ones are needed in advance to bundle them
|
||||
// Furthermore, this lets us use the latest version of all wheels including ocp-specific ones without app updates
|
||||
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`,
|
||||
packages: ["micropip", "sqlite3"], // Faster load if done here
|
||||
// _makeSnapshot: true, // Enable snapshotting for faster startup (still experimental: breaks loading any packages)
|
||||
}, (loadSnapshot ? {_loadSnapshot: loadSnapshot} : {}))); // Load snapshot if provided
|
||||
if ((import.meta as any).hot) (import.meta as any).hot.data.pyodideWorker = pyodideWorker
|
||||
} else {
|
||||
output("Reusing existing Pyodide instance...\n");
|
||||
}
|
||||
output("Preloading packages...\n");
|
||||
await pyodideWorker.asyncRun(playgroundStartupCode, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution
|
||||
running.value = false; // Indicate that Pyodide is ready
|
||||
output("Pyodide worker ready.\n");
|
||||
}
|
||||
|
||||
async function runCode() {
|
||||
if (pyodideWorker === null) {
|
||||
output("Pyodide worker is not initialized. Please wait...\n");
|
||||
return;
|
||||
}
|
||||
if (running.value) {
|
||||
output("Pyodide is already running. Please wait...\n");
|
||||
return;
|
||||
}
|
||||
output("Running code...\n");
|
||||
try {
|
||||
running.value = true;
|
||||
await pyodideWorker.asyncRun(model.value.code, output, (msg: string) => {
|
||||
// Detect models printed to console (since http server is not available in pyodide)
|
||||
if (msg.startsWith(yacvServerModelPrefix)) {
|
||||
const modelData = msg.slice(yacvServerModelPrefix.length);
|
||||
onModelData(modelData);
|
||||
} else {
|
||||
output(msg); // Print other messages directly
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
output(`Error running code: ${e}\n`);
|
||||
} finally {
|
||||
running.value = false; // Indicate that Pyodide is ready
|
||||
}
|
||||
}
|
||||
|
||||
const yacvServerModelPrefix = "yacv_server://model/";
|
||||
|
||||
function onModelData(modelData: string) {
|
||||
output(`Model data detected... ${modelData.length}B\n`);
|
||||
// Decode the model data and emit the event for the interface to handle
|
||||
// - Start by finding the end of the initial json object by looking for brackets.
|
||||
let i = 0;
|
||||
let openBrackets = 0;
|
||||
for (; i < modelData.length; i++) {
|
||||
if (modelData[i] === '{') openBrackets++;
|
||||
else if (modelData[i] === '}') openBrackets--;
|
||||
if (openBrackets === 0) break; // Found the end of the JSON object
|
||||
}
|
||||
if (openBrackets !== 0) throw `Error: Invalid model data received: ${modelData}\n`
|
||||
const jsonData = modelData.slice(0, i + 1); // Extract the JSON part and parse it into the proper class
|
||||
let modelMetadataRaw = JSON.parse(jsonData);
|
||||
const modelMetadata: any = new NetworkUpdateEventModel(modelMetadataRaw.name, "", modelMetadataRaw.hash, modelMetadataRaw.is_remove)
|
||||
// console.debug(`Model metadata:`, modelMetadata);
|
||||
output(`Model metadata: ${JSON.stringify(modelMetadata)}\n`);
|
||||
// - Now decode the rest of the model data which is a single base64 encoded glb file (or an empty string)
|
||||
if (!modelMetadata.isRemove) {
|
||||
const binaryData = Base64.toUint8Array(modelData.slice(i + 1)); // Extract the base64 part
|
||||
console.assert(binaryData.slice(0, 4).toString() == "103,108,84,70", // Ugly...
|
||||
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
|
||||
// - Create a Blob from the binary data to be used as a URL
|
||||
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
|
||||
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
|
||||
}
|
||||
// - Emit the event with the model metadata and URL
|
||||
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
|
||||
});
|
||||
emit('updateModel', networkUpdateEvent);
|
||||
}
|
||||
|
||||
function resetWorker(loadSnapshot: Uint8Array | undefined = undefined) {
|
||||
if (pyodideWorker) {
|
||||
pyodideWorker.terminate(); // Terminate existing worker
|
||||
pyodideWorker = null; // Reset worker reference
|
||||
}
|
||||
outputText.value = ``; // Clear output text
|
||||
setupPyodide(false, loadSnapshot); // Reinitialize Pyodide
|
||||
}
|
||||
|
||||
function shareLink() {
|
||||
const baseUrl = window.location
|
||||
const searchParams = new URLSearchParams(baseUrl.search);
|
||||
searchParams.delete('pg_code_url'); // Remove any existing pg_code parameter
|
||||
searchParams.delete('pg_code'); // Remove any existing pg_code parameter
|
||||
const hashParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
|
||||
hashParams.delete('pg_code_url') // Would overwrite the pg_code parameter
|
||||
hashParams.set('pg_code', b64UrlEncode(gzip(model.value.code, {level: 9}))); // Compress and encode the code
|
||||
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`; // Prefer hash to GET
|
||||
output(`Share link ready: ${shareUrl}\n`)
|
||||
if (navigator.clipboard?.writeText === undefined) {
|
||||
output("Clipboard API not available. Please copy the link manually.\n");
|
||||
return;
|
||||
} else {
|
||||
navigator.clipboard.writeText(shareUrl)
|
||||
.then(() => output("Link copied to clipboard!\n"))
|
||||
.catch(err => output(`Failed to copy link: ${err}\n`));
|
||||
}
|
||||
}
|
||||
|
||||
function saveSnapshot() {
|
||||
throw new Error("Not implemented yet!"); // TODO: Implement snapshot saving
|
||||
}
|
||||
|
||||
function loadSnapshot() {
|
||||
throw new Error("Not implemented yet!"); // TODO: Implement snapshot loading
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const sett = await settings
|
||||
if (model.value.firstTime) opacity.value = sett.pg_opacity_loading
|
||||
await setupPyodide(true);
|
||||
if (model.value.firstTime) {
|
||||
await runCode();
|
||||
opacity.value = sett.pg_opacity_loaded
|
||||
model.value.firstTime = false
|
||||
}
|
||||
})()
|
||||
|
||||
// Add keyboard shortcuts
|
||||
const editorRef = ref<HTMLElement | null>(null);
|
||||
onMounted(() => {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.addEventListener('keydown', (event: Event) => {
|
||||
if (!(event instanceof KeyboardEvent)) return; // Ensure event is a KeyboardEvent
|
||||
if (event.key === 'F10') { // Run code on F10
|
||||
event.preventDefault(); // Prevent default behavior of the key
|
||||
runCode();
|
||||
} else if (event.key === 'Escape') { // Close on Escape key
|
||||
emit('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="popup-card"
|
||||
:style="opacity == 0 ? `position: absolute; top: calc(-50vh + 24px); width: calc(100vw - 64px);` : ``">
|
||||
<v-toolbar class="popup">
|
||||
<v-toolbar-title style="flex: 0 1 auto">Playground</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<span style="display: inline-flex; margin-right: 16px;">
|
||||
<svg-icon :path="mdiCircleOpacity" type="mdi" style="height: 32px"></svg-icon>
|
||||
<v-slider v-model="opacity" :max="1" :min="0" :step="0.1"
|
||||
style="width: 100px; height: 32px">
|
||||
</v-slider>
|
||||
<v-tooltip activator="parent"
|
||||
location="bottom">Opacity of the editor (0 = hidden, 1 = fully visible)</v-tooltip>
|
||||
</span>
|
||||
|
||||
<span style="margin-right: -8px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
|
||||
<v-btn icon disabled @click="saveSnapshot()">
|
||||
<svg-icon :path="mdiContentSave" type="mdi"/>
|
||||
</v-btn>
|
||||
<v-tooltip activator="parent"
|
||||
location="bottom">Save current state to a snapshot for fast startup (WIP)</v-tooltip>
|
||||
</span>
|
||||
<span style="margin-left: -8px"><!-- This span is only needed to force tooltip to work while button is disabled -->
|
||||
<v-btn icon disabled @click="loadSnapshot()">
|
||||
<svg-icon :path="mdiFolderOpen" type="mdi"/>
|
||||
</v-btn>
|
||||
<v-tooltip activator="parent" location="bottom">Load snapshot for fast startup (WIP)</v-tooltip>
|
||||
</span>
|
||||
|
||||
<v-btn icon @click="resetWorker()">
|
||||
<svg-icon :path="mdiReload" type="mdi"/>
|
||||
<v-tooltip activator="parent" location="bottom">Reset Pyodide worker (this forgets all previous state and will
|
||||
take a little while)
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="runCode()" :disabled="running">
|
||||
<svg-icon :path="mdiPlay" type="mdi"/>
|
||||
<Loading v-if="running" style="position: absolute; top: -16%; left: -16%"/><!-- Ugly positioning -->
|
||||
<v-tooltip activator="parent" location="bottom">Run code</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="shareLink()">
|
||||
<svg-icon :path="mdiShare" type="mdi"/>
|
||||
<v-tooltip activator="parent" location="bottom">Share link that auto-runs the code</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="emit('close')">
|
||||
<svg-icon :path="mdiClose" type="mdi"/>
|
||||
<v-tooltip activator="parent" location="bottom">Close (Pyodide remains loaded)</v-tooltip>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="popup-card-text" :style="opacity == 0 ? `display: none` : ``">
|
||||
<!-- Only show content if opacity is greater than 0 -->
|
||||
<div class="playground-container">
|
||||
<div class="playground-editor" ref="editorRef">
|
||||
<VueMonacoEditor v-model:value="model.code" :theme="editorTheme" :options="MONACO_EDITOR_OPTIONS"
|
||||
language="python" @mount="handleMount"/>
|
||||
</div>
|
||||
<div class="playground-console">
|
||||
<h3 style="display:flex; align-items: center; justify-content: space-between; margin: 0;">
|
||||
Console Output
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="outputText = ''">
|
||||
<svg-icon :path="mdiBroom" type="mdi" class="h-"/>
|
||||
</v-btn>
|
||||
</h3>
|
||||
<pre>{{ outputText }}</pre> <!-- Placeholder for console output -->
|
||||
<Loading v-if="running"/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.popup-card {
|
||||
background-color: #00000000; /* Transparent background */
|
||||
}
|
||||
|
||||
.v-toolbar.popup > * {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.popup-card-text {
|
||||
background-color: #1e1e1e; /* Matches the Monaco editor background */
|
||||
opacity: v-bind(opacity);
|
||||
}
|
||||
|
||||
.playground-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.playground-editor {
|
||||
flex: 1;
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.playground-console {
|
||||
flex: 0.5;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
min-width: 100px;
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.playground-console pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (min-height: 100vw) {
|
||||
/* Adjust layout for vertical space */
|
||||
.playground-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playground-editor {
|
||||
flex: 1;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.playground-editor > * {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.playground-console {
|
||||
max-height: calc(40vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Adjust more colors on bright mode */
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* https://stackoverflow.com/questions/47017753/monaco-editor-dynamically-resizable/71876526#71876526 */
|
||||
.monaco-editor {
|
||||
position: absolute !important;
|
||||
}
|
||||
</style>
|
||||
48
frontend/tools/PlaygroundStartup.py
Normal file
48
frontend/tools/PlaygroundStartup.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import micropip
|
||||
|
||||
# Prioritize the OCP.wasm package repository for finding the ported dependencies.
|
||||
micropip.set_index_urls(["https://yeicor.github.io/OCP.wasm", "https://pypi.org/simple"])
|
||||
|
||||
# For build123d < 0.10.0, we need to install the mock the py-lib3mf package (before the main install).
|
||||
await micropip.install("lib3mf")
|
||||
micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'})
|
||||
|
||||
# Install the yacv_server package, which is the main server for the OCP.wasm playground; and also preinstalls build123d.
|
||||
await micropip.install("yacv_server", pre=True)
|
||||
|
||||
# Preimport the yacv_server package to ensure it is available in the global scope, and mock the ocp_vscode package.
|
||||
from yacv_server import *
|
||||
micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from yacv_server import *'})
|
||||
show_object = show
|
||||
|
||||
# Preinstall a font to avoid issues with no font being available.
|
||||
async def install_font_to_ocp(font_url, font_name=None):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from pyodide.http import pyfetch
|
||||
from OCP.Font import Font_FontMgr, Font_SystemFont, Font_FA_Regular
|
||||
from OCP.TCollection import TCollection_AsciiString
|
||||
import os
|
||||
|
||||
# Prepare the font path and name
|
||||
font_name = font_name if font_name is not None else font_url.split("/")[-1]
|
||||
font_path = os.path.join("/tmp", font_name)
|
||||
os.makedirs(os.path.dirname(font_path), exist_ok=True)
|
||||
|
||||
# Download the font using pyfetch
|
||||
response = await pyfetch(font_url)
|
||||
font_data = await response.bytes()
|
||||
|
||||
# Save it to the system-like folder
|
||||
with open(font_path, "wb") as f:
|
||||
f.write(font_data)
|
||||
|
||||
mgr = Font_FontMgr.GetInstance_s()
|
||||
font_t = Font_SystemFont(TCollection_AsciiString(font_path))
|
||||
font_t.SetFontPath(Font_FA_Regular, TCollection_AsciiString(font_path))
|
||||
assert mgr.RegisterFont(font_t, False)
|
||||
#print(f"✅ Font installed at: {font_path}")
|
||||
return font_path
|
||||
|
||||
|
||||
# Make sure there is at least one font installed, so that the tests can run
|
||||
await install_font_to_ocp("https://raw.githubusercontent.com/xbmc/xbmc/d3a7f95f3f017b8e861d5d95cc4b33eef4286ce2/media/Fonts/arial.ttf")
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
|
||||
import {inject, ref, type ShallowRef, watch} from "vue";
|
||||
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
@@ -28,7 +29,7 @@ let emit = defineEmits<{ findModel: [string] }>();
|
||||
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
|
||||
let selectionEnabled = ref(false);
|
||||
let selected = defineModel<Array<SelectionInfo>>({default: []});
|
||||
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||
let openNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
||||
let showDistances = ref<Boolean>(true);
|
||||
|
||||
@@ -149,7 +150,7 @@ let mouseUpListener = (event: MouseEvent) => {
|
||||
// Return the best hit
|
||||
[0] as Intersection<MObject3D> | undefined;
|
||||
|
||||
if (!highlightNextSelection.value[0]) {
|
||||
if (!openNextSelection.value[0]) {
|
||||
// If we are selecting, toggle the selection or deselect all if no hit
|
||||
let selInfo: SelectionInfo | null = null;
|
||||
if (hit) selInfo = hitToSelectionInfo(hit);
|
||||
@@ -171,7 +172,7 @@ let mouseUpListener = (event: MouseEvent) => {
|
||||
// Otherwise, highlight the model that owns the hit
|
||||
emit('findModel', hit.object.userData[extrasNameKey])
|
||||
// And reset the selection mode
|
||||
toggleHighlightNextSelection()
|
||||
toggleOpenNextSelection()
|
||||
}
|
||||
scene.queueRender() // Force rerender of model-viewer
|
||||
}
|
||||
@@ -209,17 +210,17 @@ function toggleSelection() {
|
||||
setDisableTap(selectionEnabled.value);
|
||||
}
|
||||
|
||||
function toggleHighlightNextSelection() {
|
||||
highlightNextSelection.value = [
|
||||
!highlightNextSelection.value[0],
|
||||
highlightNextSelection.value[0] ? highlightNextSelection.value[1] : selectionEnabled.value
|
||||
function toggleOpenNextSelection() {
|
||||
openNextSelection.value = [
|
||||
!openNextSelection.value[0],
|
||||
openNextSelection.value[0] ? openNextSelection.value[1] : selectionEnabled.value
|
||||
];
|
||||
if (highlightNextSelection.value[0]) {
|
||||
if (openNextSelection.value[0]) {
|
||||
// Reuse selection code to identify the model
|
||||
if (!selectionEnabled.value) toggleSelection()
|
||||
} else {
|
||||
if (selectionEnabled.value !== highlightNextSelection.value[1]) toggleSelection()
|
||||
highlightNextSelection.value = [false, false];
|
||||
if (selectionEnabled.value !== openNextSelection.value[1]) toggleSelection()
|
||||
openNextSelection.value = [false, false];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +334,7 @@ function updateBoundingBox() {
|
||||
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
|
||||
edge = axisEdges[0];
|
||||
let edgeDist = Infinity;
|
||||
let cameraPos: Vector3 = props.viewer?.scene.camera.position;
|
||||
let cameraPos: Vector3 = props.viewer?.scene?.camera?.position ?? new Vector3();
|
||||
for (let testEdge of axisEdges) {
|
||||
let from = new Vector3(...corners[testEdge[0]]);
|
||||
let to = new Vector3(...corners[testEdge[1]]);
|
||||
@@ -427,6 +428,10 @@ defineExpose({deselect, updateBoundingBox, updateDistances});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if ((event.target as any)?.tagName && ((event.target as any).tagName === 'INPUT' || (event.target as any).tagName === 'TEXTAREA')) {
|
||||
// Ignore key events when an input is focused, except for text inputs
|
||||
return;
|
||||
}
|
||||
if (event.key === 's') {
|
||||
if (selectFilter.value == 'Any (S)') toggleSelection();
|
||||
else {
|
||||
@@ -455,8 +460,8 @@ window.addEventListener('keydown', (event) => {
|
||||
toggleShowBoundingBox();
|
||||
} else if (event.key === 'd') {
|
||||
toggleShowDistances();
|
||||
} else if (event.key === 'h') {
|
||||
toggleHighlightNextSelection();
|
||||
} else if (event.key === 'o') {
|
||||
toggleOpenNextSelection();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -474,8 +479,8 @@ window.addEventListener('keydown', (event) => {
|
||||
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>
|
||||
<v-btn :color="openNextSelection[0] ? 'surface-light' : ''" icon @click="toggleOpenNextSelection">
|
||||
<v-tooltip activator="parent">(O)pen 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">
|
||||
|
||||
@@ -13,13 +13,26 @@ import {
|
||||
import OrientationGizmo from "./OrientationGizmo.vue";
|
||||
import type {PerspectiveCamera} from "three/src/cameras/PerspectiveCamera.js";
|
||||
import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
|
||||
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
|
||||
import {
|
||||
mdiClose,
|
||||
mdiCrosshairsGps,
|
||||
mdiDownload,
|
||||
mdiGithub,
|
||||
mdiLicense,
|
||||
mdiLightbulb,
|
||||
mdiProjector,
|
||||
mdiScriptTextPlay
|
||||
} from '@mdi/js'
|
||||
// @ts-expect-error
|
||||
import SvgIcon from '@jamescoyle/vue-icon';
|
||||
import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import Loading from "../misc/Loading.vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {defineAsyncComponent, ref, type Ref} from "vue";
|
||||
import {defineAsyncComponent, ref} from "vue";
|
||||
import type {SelectionInfo} from "./selection";
|
||||
import {settings} from "../misc/settings.ts";
|
||||
import type {NetworkUpdateEvent} from "../misc/network.ts";
|
||||
import IfNotSmallBuild from "../misc/IfNotSmallBuild.vue";
|
||||
|
||||
const SelectionComponent = defineAsyncComponent({
|
||||
loader: () => import("./Selection.vue"),
|
||||
@@ -34,11 +47,26 @@ const LicensesDialogContent = defineAsyncComponent({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
const PlaygroundDialogContent = defineAsyncComponent({
|
||||
loader: () => import("./PlaygroundDialogContent.vue"),
|
||||
loadingComponent: Loading,
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
|
||||
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
|
||||
const emit = defineEmits<{ findModel: [string] }>()
|
||||
const emit = defineEmits<{ findModel: [string], updateModel: [NetworkUpdateEvent] }>()
|
||||
|
||||
let selection: Ref<Array<SelectionInfo>> = ref([]);
|
||||
const sett = ref<any | null>(null);
|
||||
const showPlaygroundDialog = ref(false);
|
||||
const pg_model = ref({code: '# Loading...', firstTime: false});
|
||||
(async () => {
|
||||
sett.value = await settings;
|
||||
pg_model.value = {code: sett.value.pg_code, firstTime: true};
|
||||
showPlaygroundDialog.value = pg_model.value.code != "";
|
||||
})();
|
||||
|
||||
let selection = ref<Array<SelectionInfo>>([]);
|
||||
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
|
||||
@@ -100,6 +128,7 @@ async function downloadSceneGlb() {
|
||||
link.download = file.name;
|
||||
link.href = URL.createObjectURL(file);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
async function openGithub() {
|
||||
@@ -114,10 +143,14 @@ function removeObjectSelections(objName: string) {
|
||||
selectionComp.value?.updateDistances();
|
||||
}
|
||||
|
||||
defineExpose({removeObjectSelections});
|
||||
defineExpose({removeObjectSelections, openPlayground: () => showPlaygroundDialog.value = true});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if ((event.target as any)?.tagName && ((event.target as any).tagName === 'INPUT' || (event.target as any).tagName === 'TEXTAREA')) {
|
||||
// Ignore key events when an input is focused, except for text inputs
|
||||
return;
|
||||
}
|
||||
if (event.key === 'p') toggleProjection();
|
||||
else if (event.key === 'c') centerCamera();
|
||||
else if (event.key === 'd') downloadSceneGlb();
|
||||
@@ -139,6 +172,13 @@ window.addEventListener('keydown', (event) => {
|
||||
<v-tooltip activator="parent">Re(c)enter Camera</v-tooltip>
|
||||
<svg-icon :path="mdiCrosshairsGps" type="mdi"/>
|
||||
</v-btn>
|
||||
<span>
|
||||
<v-tooltip activator="parent">To rotate the light hold shift and drag the mouse or use two fingers<br/>
|
||||
Note that this breaks slightly clipping planes for now... (restart to fix)</v-tooltip>
|
||||
<v-btn icon disabled style="background: black;">
|
||||
<svg-icon :path="mdiLightbulb" type="mdi"/>
|
||||
</v-btn>
|
||||
</span>
|
||||
<v-divider/>
|
||||
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
|
||||
<selection-component ref="selectionComp" v-model="selection" :viewer="props.viewer as any"
|
||||
@@ -146,11 +186,26 @@ window.addEventListener('keydown', (event) => {
|
||||
<v-divider/>
|
||||
<v-spacer></v-spacer>
|
||||
<h5>Extras</h5>
|
||||
<v-dialog v-model="showPlaygroundDialog" persistent :scrim="false" attach="body">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" style="width: 100%">
|
||||
<v-tooltip activator="parent">Open a python editor and build models directly in the browser!</v-tooltip>
|
||||
<svg-icon :path="mdiScriptTextPlay" type="mdi"/>
|
||||
Sandbox
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<if-not-small-build>
|
||||
<playground-dialog-content v-if="sett != null" v-model="pg_model" @close="isActive.value = false"
|
||||
@update-model="(event: NetworkUpdateEvent) => emit('updateModel', event)"/>
|
||||
</if-not-small-build>
|
||||
</template>
|
||||
</v-dialog>
|
||||
<v-btn icon @click="downloadSceneGlb">
|
||||
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
|
||||
<svg-icon :path="mdiDownload" type="mdi"/>
|
||||
</v-btn>
|
||||
<v-dialog id="licenses-dialog" fullscreen>
|
||||
<v-dialog>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props">
|
||||
<v-tooltip activator="parent">Show Licenses</v-tooltip>
|
||||
@@ -158,11 +213,10 @@ window.addEventListener('keydown', (event) => {
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card>
|
||||
<v-toolbar>
|
||||
<v-card style="height: 90vh">
|
||||
<v-toolbar class="popup">
|
||||
<v-toolbar-title>Licenses</v-toolbar-title>
|
||||
<v-spacer>
|
||||
</v-spacer>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="isActive.value = false">
|
||||
<svg-icon :path="mdiClose" type="mdi"/>
|
||||
</v-btn>
|
||||
@@ -199,4 +253,17 @@ window.addEventListener('keydown', (event) => {
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.v-toolbar {
|
||||
position: sticky !important;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.v-toolbar.popup {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.v-toolbar.popup > div {
|
||||
height: 32px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
21
frontend/tools/b64.ts
Normal file
21
frontend/tools/b64.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function b64UrlEncode(data: Uint8Array): string {
|
||||
const base64 = btoa(String.fromCharCode(...data));
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
export function b64UrlDecode(encoded: string): Uint8Array {
|
||||
// Replace URL-safe characters with standard base64 characters
|
||||
let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
||||
// Add padding if necessary
|
||||
const padding = base64.length % 4;
|
||||
if (padding) {
|
||||
base64 += '='.repeat(4 - padding);
|
||||
}
|
||||
// Decode the base64 string to a byte array
|
||||
const binaryString = atob(base64);
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
byteArray[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
35
frontend/tools/monaco.ts
Normal file
35
frontend/tools/monaco.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {loader} from "@guolao/vue-monaco-editor"
|
||||
|
||||
import * as monaco from "monaco-editor"
|
||||
//@ts-ignore
|
||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"
|
||||
//@ts-ignore
|
||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"
|
||||
//@ts-ignore
|
||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"
|
||||
//@ts-ignore
|
||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"
|
||||
//@ts-ignore
|
||||
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
if (label === "json") {
|
||||
return new jsonWorker()
|
||||
}
|
||||
if (label === "css" || label === "scss" || label === "less") {
|
||||
return new cssWorker()
|
||||
}
|
||||
if (label === "html" || label === "handlebars" || label === "razor") {
|
||||
return new htmlWorker()
|
||||
}
|
||||
if (label === "typescript" || label === "javascript") {
|
||||
return new tsWorker()
|
||||
}
|
||||
return new editorWorker()
|
||||
}
|
||||
}
|
||||
|
||||
export function setupMonaco() {
|
||||
loader.config({monaco})
|
||||
}
|
||||
45
frontend/tools/pyodide-worker-api.ts
Normal file
45
frontend/tools/pyodide-worker-api.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type {loadPyodide} from "pyodide";
|
||||
import type {MessageEventDataIn} from "./pyodide-worker.ts";
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
/** Simple API for the Pyodide worker. */
|
||||
export function newPyodideWorker(initOpts: Parameters<typeof loadPyodide>[0]) {
|
||||
let worker = new Worker(new URL('./pyodide-worker.ts', import.meta.url), {type: "module"});
|
||||
worker.postMessage(initOpts);
|
||||
const commonRequestResponse = (event: MessageEventDataIn, stdout?: (msg: string) => void, stderr?: (msg: string) => void) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.addEventListener("message", function listener(event: MessageEvent) {
|
||||
if (stdout && event.data?.stdout) {
|
||||
stdout(event.data.stdout); // No clue if associated with this request, but we handle it anyway.
|
||||
return;
|
||||
}
|
||||
if (stderr && event.data?.stderr) {
|
||||
stderr(event.data.stderr); // No clue if associated with this request, but we handle it anyway.
|
||||
return;
|
||||
}
|
||||
if (event.data?.id !== event.data.id) return; // Ignore messages that are not for this request.
|
||||
if (event.data?.error) {
|
||||
worker.removeEventListener("message", listener);
|
||||
reject(event.data.error);
|
||||
} else if (event.data?.hasOwnProperty("result")) {
|
||||
worker.removeEventListener("message", listener);
|
||||
resolve(event.data.result);
|
||||
} else {
|
||||
throw new Error("Unexpected message from worker: " + JSON.stringify(event.data));
|
||||
}
|
||||
})
|
||||
worker.postMessage(event);
|
||||
});
|
||||
}
|
||||
return {
|
||||
asyncRun: (code: string, stdout: (msg: string) => void, stderr: (msg: string) => void) =>
|
||||
commonRequestResponse({type: "asyncRun", id: requestId++, code}, stdout, stderr),
|
||||
mkdirTree: (path: string) => commonRequestResponse({type: "mkdirTree", id: requestId++, path}),
|
||||
writeFile: (path: string, content: string) =>
|
||||
commonRequestResponse({type: "writeFile", id: requestId++, path, content}),
|
||||
makeSnapshot: () => commonRequestResponse({type: "makeSnapshot", id: requestId++}),
|
||||
terminate: () => worker.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
83
frontend/tools/pyodide-worker.ts
Normal file
83
frontend/tools/pyodide-worker.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {loadPyodide, type PyodideInterface} from "pyodide";
|
||||
|
||||
let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide({
|
||||
...initOpts,
|
||||
stdout: (msg) => self.postMessage({stdout: msg + "\n"}), // Add newline for better readability
|
||||
stderr: (msg) => self.postMessage({stderr: msg + "\n"}), // Add newline for better readability
|
||||
stdin: () => {
|
||||
console.warn("Input requested by Python code, but stdin is not supported in this playground.");
|
||||
return "";
|
||||
},
|
||||
});
|
||||
|
||||
let pyodideReadyPromise: Promise<PyodideInterface> | null = null;
|
||||
|
||||
export type MessageEventDataIn = {
|
||||
type: 'asyncRun';
|
||||
id: number;
|
||||
code: string;
|
||||
} | {
|
||||
type: 'mkdirTree';
|
||||
id: number;
|
||||
path: string;
|
||||
} | {
|
||||
type: 'writeFile';
|
||||
id: number;
|
||||
path: string;
|
||||
content: string;
|
||||
} | {
|
||||
type: 'makeSnapshot';
|
||||
id: number;
|
||||
}
|
||||
|
||||
self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
||||
if (!pyodideReadyPromise) { // First message is always the init message
|
||||
// If we haven't loaded Pyodide yet, do so now.
|
||||
// This is a singleton, so we only load it once.
|
||||
pyodideReadyPromise = myLoadPyodide(event.data as Parameters<typeof loadPyodide>[0]);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'mkdirTree') {
|
||||
// Create a directory tree in the Pyodide filesystem.
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
try {
|
||||
pyodide.FS.mkdirTree(event.data.path);
|
||||
self.postMessage({id: event.data.id, result: true});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
}
|
||||
return;
|
||||
} else if (event.data.type === 'writeFile') {
|
||||
// Write a file to the Pyodide filesystem.
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
try {
|
||||
pyodide.FS.writeFile(event.data.path, event.data.content);
|
||||
self.postMessage({id: event.data.id, result: true});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
}
|
||||
} else if (event.data.type === 'asyncRun') {
|
||||
let code = event.data.code;
|
||||
// make sure loading is done
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
// Now load any packages we need, run the code, and send the result back.
|
||||
await pyodide.loadPackagesFromImports(code);
|
||||
try {
|
||||
self.postMessage({id: event.data.id, result: await pyodide.runPythonAsync(code)});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
}
|
||||
} else if (event.data.type === 'makeSnapshot') {
|
||||
// Take a snapshot of the current Pyodide filesystem.
|
||||
const pyodide = await pyodideReadyPromise;
|
||||
try {
|
||||
const snapshot = pyodide.makeMemorySnapshot();
|
||||
self.postMessage({id: event.data.id, result: snapshot});
|
||||
} catch (error: any) {
|
||||
self.postMessage({id: event.data.id, error: error.message});
|
||||
}
|
||||
} else {
|
||||
console.error("Unknown message type:", (event.data as any)?.type);
|
||||
self.postMessage({id: (event.data as any)?.id, error: "Unknown message type: " + (event.data as any)?.type});
|
||||
}
|
||||
};
|
||||
@@ -78,7 +78,7 @@ export function hitToSelectionInfo(hit: Intersection<MObject3D>): SelectionInfo
|
||||
|
||||
function hitFaceTriangleIndices(hit: Intersection<MObject3D>): [number, number] | null {
|
||||
let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
|
||||
if (hit.faceIndex === undefined) return null;
|
||||
if (!hit.faceIndex) return null;
|
||||
if (!faceTrianglesEnd) { // Fallback to selecting the whole imported mesh
|
||||
//console.log("No face_triangles_end found, selecting the whole mesh");
|
||||
return [0, (hit.object.geometry.index ?? hit.object.geometry.attributes.position).count];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import {settings} from "../misc/settings";
|
||||
import {inject, onMounted, type Ref, ref, watch} from "vue";
|
||||
import {inject, onUpdated, type Ref, ref, watch} from "vue";
|
||||
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||
import {$controls} from '@google/model-viewer/lib/features/controls.js';
|
||||
import {type SmoothControls} from '@google/model-viewer/lib/three-components/SmoothControls';
|
||||
@@ -11,6 +11,7 @@ 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';
|
||||
import {setupLighting} from "./lighting.ts";
|
||||
|
||||
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
||||
//@ts-ignore
|
||||
@@ -27,14 +28,19 @@ const scene = ref<ModelScene | null>(null);
|
||||
const renderer = ref<Renderer | null>(null);
|
||||
const controls = ref<SmoothControls | null>(null);
|
||||
|
||||
const sett = ref<any | null>(null);
|
||||
(async () => sett.value = await settings)();
|
||||
|
||||
let lastCameraTargetPosition: Vector3 | undefined = undefined;
|
||||
let lastCameraZoom: number | undefined = undefined;
|
||||
let lastCameraUrl = props.src.toString();
|
||||
onMounted(() => {
|
||||
if (!elem.value) return;
|
||||
let initialized = false
|
||||
onUpdated(() => {
|
||||
if (!elem.value) return; // Not ready yet
|
||||
if (initialized) return; // Already initialized
|
||||
initialized = true;
|
||||
elem.value.addEventListener('before-render', () => {
|
||||
if (!elem.value) return;
|
||||
if (!elem.value) return
|
||||
// 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;
|
||||
@@ -62,6 +68,7 @@ onMounted(() => {
|
||||
});
|
||||
elem.value.addEventListener('camera-change', onCameraChange);
|
||||
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
|
||||
setupLighting(elem.value);
|
||||
});
|
||||
|
||||
function onCameraChange() {
|
||||
@@ -207,12 +214,12 @@ watch(disableTap, (newDisableTap) => {
|
||||
|
||||
<template>
|
||||
<!-- The main 3D model viewer -->
|
||||
<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"
|
||||
<model-viewer ref="elem" v-if="sett != null" :ar="sett.arModes.length > 0" :ar-modes="sett.arModes"
|
||||
:environment-image="sett.environment" :exposure="sett.exposure" :autoplay="sett.autoplay"
|
||||
:orbit-sensitivity="sett.orbitSensitivity" :pan-sensitivity="sett.panSensitivity"
|
||||
:poster="poster" :shadow-intensity="sett.shadowIntensity" :skybox-image="sett.skybox"
|
||||
:src="props.src" :zoom-sensitivity="sett.zoomSensitivity" alt="The 3D model(s)" camera-controls
|
||||
camera-orbit="45deg 45deg 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 -->
|
||||
@@ -297,4 +304,4 @@ watch(disableTap, (newDisableTap) => {
|
||||
float: left;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
76
frontend/viewer/lighting.ts
Normal file
76
frontend/viewer/lighting.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {$scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||
import {settings} from "../misc/settings.ts";
|
||||
|
||||
export let currentSceneRotation = 0; // radians, 0 is the default rotation
|
||||
|
||||
export async function setupLighting(modelViewer: ModelViewerElement) {
|
||||
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
|
||||
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
|
||||
let lastX: number;
|
||||
let panning = false;
|
||||
let radiansPerPixel: number;
|
||||
|
||||
const startPan = () => {
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
const {radius} = orbit;
|
||||
radiansPerPixel = -1 * radius / modelViewer.getBoundingClientRect().height;
|
||||
modelViewer.interactionPrompt = 'none';
|
||||
};
|
||||
|
||||
const updatePan = (thisX: number) => {
|
||||
const delta = (thisX - lastX) * radiansPerPixel;
|
||||
lastX = thisX;
|
||||
currentSceneRotation += delta;
|
||||
const orbit = modelViewer.getCameraOrbit();
|
||||
orbit.theta += delta;
|
||||
modelViewer.cameraOrbit = orbit.toString();
|
||||
modelViewer.resetTurntableRotation(currentSceneRotation);
|
||||
modelViewer.jumpCameraToGoal();
|
||||
}
|
||||
|
||||
modelViewer.addEventListener('mousedown', (event) => {
|
||||
panning = event.metaKey || event.shiftKey;
|
||||
if (!panning)
|
||||
return;
|
||||
|
||||
lastX = event.clientX;
|
||||
startPan();
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
|
||||
modelViewer.addEventListener('touchstart', (event) => {
|
||||
const {targetTouches, touches} = event;
|
||||
panning = targetTouches.length === 2 && targetTouches.length === touches.length;
|
||||
if (!panning)
|
||||
return;
|
||||
|
||||
lastX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
|
||||
startPan();
|
||||
}, true);
|
||||
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
if (!panning)
|
||||
return;
|
||||
|
||||
updatePan(event.clientX);
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
|
||||
modelViewer.addEventListener('touchmove', (event) => {
|
||||
if (!panning || event.targetTouches.length !== 2)
|
||||
return;
|
||||
|
||||
const {targetTouches} = event;
|
||||
const thisX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
|
||||
updatePan(thisX);
|
||||
}, true);
|
||||
|
||||
document.addEventListener('mouseup', (event) => {
|
||||
panning = false;
|
||||
}, true);
|
||||
|
||||
modelViewer.addEventListener('touchend', (event) => {
|
||||
panning = false;
|
||||
}, true);
|
||||
}
|
||||
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yet-another-cad-viewer",
|
||||
"version": "0.8.11",
|
||||
"version": "0.10.1",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -15,33 +15,40 @@
|
||||
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gltf-transform/core": "^4.0.4",
|
||||
"@gltf-transform/extensions": "^4.0.8",
|
||||
"@gltf-transform/functions": "^4.0.8",
|
||||
"@google/model-viewer": "^3.5.0",
|
||||
"@gltf-transform/core": "^4.1.0",
|
||||
"@gltf-transform/extensions": "^4.1.0",
|
||||
"@gltf-transform/functions": "^4.1.0",
|
||||
"@google/model-viewer": "^4.0.0",
|
||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
||||
"@jamescoyle/vue-icon": "^0.1.2",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/svg": "^7.4.47",
|
||||
"three": "^0.163.0",
|
||||
"three-mesh-bvh": "^0.8.0",
|
||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||
"vue": "^3.5.7",
|
||||
"vuetify": "^3.7.2"
|
||||
"js-base64": "^3.7.7",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pako": "^2.1.0",
|
||||
"pyodide": "^0.28.0",
|
||||
"three": "^0.178.0",
|
||||
"three-mesh-bvh": "^0.9.0",
|
||||
"three-orientation-gizmo": "git+https://github.com/jrj2211/three-orientation-gizmo.git",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/three": "^0.163.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@types/node": "^22.9.3",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/three": "^0.178.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"buffer": "^5.5.0||^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"generate-license-file": "^3.5.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"terser": "^5.33.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.7",
|
||||
"vue-tsc": "^2.1.6"
|
||||
"commander": "^14.0.0",
|
||||
"generate-license-file": "^4.0.0",
|
||||
"npm-run-all2": "^8.0.0",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-static-copy": "^3.1.1",
|
||||
"vue-tsc": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
1495
poetry.lock
generated
1495
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,34 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core==2.1.3", "taskipy==1.14.1"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "yacv-server"
|
||||
version = "0.8.11"
|
||||
version = "0.10.1"
|
||||
description = "Yet Another CAD Viewer (server)"
|
||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
include = [
|
||||
{ path = 'yacv_server/frontend/*', format = 'wheel' },
|
||||
{ path = 'yacv_server/frontend/*', format = 'sdist' },
|
||||
{ path = 'yacv_server/frontend/**/*', format = 'wheel' },
|
||||
{ path = 'yacv_server/frontend/**/*', format = 'sdist' },
|
||||
]
|
||||
|
||||
[tool.taskipy.tasks]
|
||||
build = "task build_frontend && task build_backend"
|
||||
build_frontend = "rm -rf yacv_server/frontend || true && yarn install && YACV_SMALL_BUILD=true yarn build --outDir yacv_server/frontend"
|
||||
build_backend = "poetry build --format wheel"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
python = ">=3.12,<3.13" # Due to vtk transitive dependency of build123d -> cadquery-ocp -> vtk
|
||||
|
||||
# CAD
|
||||
build123d = ">=0.5,<0.8"
|
||||
build123d = ">=0.9,<0.10"
|
||||
|
||||
# Misc
|
||||
pygltflib = "^1.16.2"
|
||||
pillow = "^10.2.0"
|
||||
pillow = ">=10.2,<12.0"
|
||||
poetry-core = "==2.1.3"
|
||||
|
||||
[tool.poetry.build]
|
||||
generate-setup-file = false
|
||||
script = "build.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
taskipy = "^1.14.1"
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
|
||||
import {defineConfig} from 'vite'
|
||||
// @ts-ignore
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
// @ts-ignore
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import {name, version} from './package.json'
|
||||
import {execSync} from 'child_process'
|
||||
import {viteStaticCopy} from "vite-plugin-static-copy";
|
||||
import {dirname, join} from "path";
|
||||
import {version as pyodideVersion} from "pyodide";
|
||||
|
||||
let wantsSmallBuild = process.env.YACV_SMALL_BUILD == "true";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -13,12 +20,14 @@ export default defineConfig({
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag == 'model-viewer'
|
||||
isCustomElement: (tag: string) => tag == 'model-viewer'
|
||||
}
|
||||
}
|
||||
}),
|
||||
vueJsx(),
|
||||
viteStaticCopyPyodide(),
|
||||
],
|
||||
optimizeDeps: {exclude: ["pyodide"]},
|
||||
resolve: {
|
||||
alias: {
|
||||
// @ts-ignore
|
||||
@@ -28,13 +37,51 @@ export default defineConfig({
|
||||
build: {
|
||||
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.
|
||||
chunkSizeWarningLimit: 1024, // KB. Three.js is big. Draco is even bigger but not likely to be used.
|
||||
sourcemap: true, // For debugging production
|
||||
rollupOptions: {
|
||||
output: {
|
||||
experimentalMinChunkSize: 512000, // 512KB (avoid too many small chunks)
|
||||
},
|
||||
external: wantsSmallBuild ? [
|
||||
// Exclude some large optional dependencies if small build is requested (for embedding in python package)
|
||||
"pyodide",
|
||||
/.*\/pyodide-worker.*/,
|
||||
"monaco-editor",
|
||||
/monaco-editor\/.*/,
|
||||
"@guolao/vue-monaco-editor",
|
||||
/three\/examples\/jsm\/libs\/draco\/draco_(en|de)coder\.js/,
|
||||
] : [],
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
format: 'es', // Use ES modules for workers (IIFE is not supported with code-splitting)
|
||||
},
|
||||
define: {
|
||||
__APP_NAME__: JSON.stringify(name),
|
||||
__APP_VERSION__: JSON.stringify(version),
|
||||
__APP_GIT_SHA__: JSON.stringify(execSync('git rev-parse HEAD').toString().trim()),
|
||||
__APP_GIT_DIRTY__: JSON.stringify(execSync('git diff --quiet || echo dirty').toString().trim()),
|
||||
__YACV_SMALL_BUILD__: JSON.stringify(wantsSmallBuild),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function viteStaticCopyPyodide() {
|
||||
const PYODIDE_EXCLUDE = [
|
||||
"!**/*.{md,html}",
|
||||
"!**/*.d.ts",
|
||||
"!**/*.whl",
|
||||
"!**/node_modules",
|
||||
];
|
||||
// @ts-ignore
|
||||
const pyodideDir = dirname(fileURLToPath(import.meta.resolve("pyodide")));
|
||||
return viteStaticCopy({
|
||||
targets: wantsSmallBuild ? [] : [
|
||||
{
|
||||
src: [join(pyodideDir, "*")].concat(PYODIDE_EXCLUDE),
|
||||
dest: "pyodide-v" + pyodideVersion, // It would be better to use hashed names instead of folder...
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,12 +10,30 @@ 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 build123d import Compound, Color
|
||||
|
||||
from yacv_server.gltf import GLTFMgr
|
||||
|
||||
CADCoreLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and Locations for now
|
||||
CADLike = Union[CADCoreLike, any] # build123d and cadquery types
|
||||
ColorTuple = Tuple[float, float, float, float]
|
||||
|
||||
|
||||
def get_color(obj: any) -> Optional[ColorTuple]:
|
||||
"""Get color from a CAD Object or any other color-like object"""
|
||||
if 'color' in dir(obj):
|
||||
obj = obj.color
|
||||
if isinstance(obj, tuple):
|
||||
c = None
|
||||
if len(obj) == 3:
|
||||
c = obj + (1,)
|
||||
elif len(obj) == 4:
|
||||
c = obj
|
||||
# noinspection PyTypeChecker
|
||||
return [min(max(float(x), 0), 1) for x in c]
|
||||
if isinstance(obj, Color):
|
||||
return obj.to_tuple()
|
||||
return None
|
||||
|
||||
|
||||
def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
||||
@@ -59,8 +77,9 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
||||
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]
|
||||
# Build a single compound shape (skip locations/axes here, they can't be in a Compound)
|
||||
shapes_bd = [Compound(shape) for shape in shapes_raw_filtered_sorted if
|
||||
shape is not None and not isinstance(shape, TopLoc_Location)]
|
||||
return get_shape(Compound(shapes_bd), error)
|
||||
except TypeError:
|
||||
pass
|
||||
@@ -103,11 +122,11 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
hasher = hashlib.md5()
|
||||
hasher.update(source)
|
||||
name = 'image_' + hasher.hexdigest()
|
||||
format: str
|
||||
_format: str
|
||||
if save_mime == 'image/jpeg':
|
||||
format = 'JPEG'
|
||||
_format = 'JPEG'
|
||||
elif save_mime == 'image/png':
|
||||
format = 'PNG'
|
||||
_format = 'PNG'
|
||||
else:
|
||||
raise ValueError(f'Unsupported save MIME type (for GLTF files): {save_mime}')
|
||||
|
||||
@@ -136,7 +155,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
img = img.resize((new_width, new_height))
|
||||
|
||||
# Save the image to a buffer
|
||||
img.save(img_buf, format=format)
|
||||
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)
|
||||
@@ -150,7 +169,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
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.z_dir)] * 4, [
|
||||
(0, 2, 1),
|
||||
(0, 3, 2),
|
||||
], [
|
||||
|
||||
@@ -4,9 +4,12 @@ import numpy as np
|
||||
from build123d import Location, Plane, Vector
|
||||
from pygltflib import *
|
||||
|
||||
_checkerboard_image_bytes = base64.decodebytes(
|
||||
b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf'
|
||||
b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=')
|
||||
|
||||
def get_version() -> str:
|
||||
try:
|
||||
return importlib.metadata.version("yacv_server")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
class GLTFMgr:
|
||||
@@ -18,6 +21,7 @@ class GLTFMgr:
|
||||
# - Face data
|
||||
face_indices: List[int] # 3 indices per triangle
|
||||
face_positions: List[float] # x, y, z
|
||||
face_normals: 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
|
||||
@@ -30,9 +34,9 @@ class GLTFMgr:
|
||||
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')):
|
||||
def __init__(self, image: Optional[Tuple[bytes, str]] = None):
|
||||
self.gltf = GLTF2(
|
||||
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
||||
asset=Asset(generator=f"yacv_server@{get_version()}"),
|
||||
scene=0,
|
||||
scenes=[Scene(nodes=[0])],
|
||||
nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
|
||||
@@ -48,6 +52,7 @@ class GLTFMgr:
|
||||
)
|
||||
self.face_indices = []
|
||||
self.face_positions = []
|
||||
self.face_normals = []
|
||||
self.face_tex_coords = []
|
||||
self.face_colors = []
|
||||
self.image = image
|
||||
@@ -70,9 +75,8 @@ class GLTFMgr:
|
||||
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)):
|
||||
def add_face(self, vertices_raw: List[Vector], normals: List[Vector], indices_raw: List[Tuple[int, int, int]],
|
||||
tex_coord_raw: List[Tuple[float, float]], color: Tuple[float, float, float, float]):
|
||||
"""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)}"
|
||||
@@ -80,12 +84,13 @@ class GLTFMgr:
|
||||
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_normals.extend([n for t in normals for n 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)):
|
||||
color: Tuple[float, float, float, float]):
|
||||
"""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
|
||||
@@ -94,8 +99,7 @@ class GLTFMgr:
|
||||
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)):
|
||||
def add_vertex(self, vertex: Tuple[float, float, float], color: Tuple[float, float, float, float]):
|
||||
"""Add a vertex to the GLTF mesh"""
|
||||
base_index = len(self.vertex_positions) // 3
|
||||
self.vertex_indices.append(base_index)
|
||||
@@ -110,10 +114,11 @@ class GLTFMgr:
|
||||
return v.X, v.Y, v.Z
|
||||
|
||||
# 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))], 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))
|
||||
# The colors are hardcoded. You can add vertices and edges manually to change them.
|
||||
self.add_vertex(vert(pl.origin), color=(0.1, 0.1, 0.1, 1.0))
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1.0))
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1.0))
|
||||
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1.0))
|
||||
|
||||
def build(self) -> GLTF2:
|
||||
"""Merge the intermediate data into the GLTF object and return it"""
|
||||
@@ -124,6 +129,8 @@ class GLTFMgr:
|
||||
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.NORMAL = len(buffers_list)
|
||||
buffers_list.append(_gen_buffer_metadata(self.face_normals, 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)
|
||||
@@ -162,6 +169,7 @@ class GLTFMgr:
|
||||
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)]
|
||||
# noinspection PyPep8Naming
|
||||
self.gltf.materials[0].pbrMetallicRoughness.baseColorTexture = TextureInfo(index=0)
|
||||
buffers_list.append((Accessor(), BufferView(), self.image[0]))
|
||||
|
||||
|
||||
@@ -6,28 +6,41 @@ from build123d import *
|
||||
ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets'))
|
||||
|
||||
|
||||
def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
|
||||
def build_logo(text: bool = True) -> Dict[str, Union[Compound, Location, str]]:
|
||||
"""Builds the CAD part of the logo"""
|
||||
with BuildPart(Plane.XY.offset(50)) as logo_obj:
|
||||
Box(22, 40, 30)
|
||||
fillet(edges().filter_by(Axis.Y).group_by(Axis.Z)[-1], 10)
|
||||
offset(solid(), 2, openings=faces().group_by(Axis.Z)[0] + faces().filter_by(Plane.XZ))
|
||||
if text:
|
||||
text_at_plane = Plane.YZ
|
||||
text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center()
|
||||
with BuildSketch(text_at_plane.location):
|
||||
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
|
||||
with BuildSketch(Plane.YZ.move(Pos(faces().group_by(Axis.X)[-1].face().center()))):
|
||||
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
|
||||
extrude(amount=1)
|
||||
logo_face_curved_front = faces().filter_by(GeomType.CYLINDER).group_by(Axis.X)[-1].face()
|
||||
|
||||
# Highlight text edges with a custom color
|
||||
to_highlight = logo_obj.edges().group_by(Axis.X)[-1]
|
||||
logo_obj_hl = Compound(to_highlight).translate((1e-3, 0, 0)) # To avoid z-fighting
|
||||
logo_obj_hl.color = (0, 0.3, 0.3, 1)
|
||||
|
||||
# Highlight face with custom texture
|
||||
logo_face_curved_front.yacv_texture = \
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf' \
|
||||
'/////48WMATwULS8tcyj8AAAAASUVORK5CYII='
|
||||
logo_face_curved_front.color = (0, 0.5, 0.0, 1)
|
||||
logo_obj = Compound(logo_obj.faces() - ShapeList([logo_face_curved_front])) # Remove face from the main object
|
||||
|
||||
# Add a logo image to the CAD part
|
||||
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_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
|
||||
|
||||
# Add an animated fox to the CAD part
|
||||
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_glb_bytes}
|
||||
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'logo_hl_tex': logo_face_curved_front,
|
||||
'location': logo_img_location, img_name: img_glb_bytes}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from os import system
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Just a reminder that a hot-reloading server can be started with the following command:
|
||||
# Need to disable auto-start to avoid conflicts with the hot-reloading server
|
||||
system('YACV_DISABLE_SERVER=true aiohttp-devtools runserver __init__.py --port 32323 --app-factory _get_app')
|
||||
@@ -1,7 +1,7 @@
|
||||
import io
|
||||
import os
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from http import HTTPStatus, HTTPMethod
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
from yacv_server.mylogger import logger
|
||||
@@ -71,6 +71,19 @@ 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")
|
||||
if not self.requestline.startswith(HTTPMethod.HEAD):
|
||||
# Chunked transfer encoding!
|
||||
self.send_header("Transfer-Encoding", "chunked")
|
||||
else:
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
if self.requestline.startswith(HTTPMethod.HEAD):
|
||||
return
|
||||
|
||||
# Keep a shared read lock to know if any frontend is still working before shutting down
|
||||
with self.yacv.frontend_lock.r_locked():
|
||||
|
||||
@@ -81,13 +94,6 @@ class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
self.yacv.at_least_one_client.set()
|
||||
logger.debug('Updates client connected')
|
||||
|
||||
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()
|
||||
|
||||
def write_chunk(_chunk_data: str):
|
||||
self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8'))
|
||||
self.wfile.write(b'\r\n')
|
||||
@@ -107,7 +113,7 @@ class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
# noinspection PyUnresolvedReferences
|
||||
to_send = data.to_json()
|
||||
write_chunk(f'data: {to_send}\n\n')
|
||||
except BrokenPipeError: # Client disconnected normally
|
||||
except (BrokenPipeError, ConnectionResetError): # Client disconnected normally
|
||||
pass
|
||||
finally:
|
||||
subscription.close()
|
||||
@@ -132,3 +138,4 @@ class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
self.send_header('E-Tag', f'"{_hash}"')
|
||||
self.end_headers()
|
||||
self.wfile.write(exported_glb)
|
||||
return None
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import queue
|
||||
import queue
|
||||
import threading
|
||||
from typing import List, TypeVar, \
|
||||
Generic, Generator
|
||||
from typing import List, TypeVar, Generic, Generator
|
||||
|
||||
from yacv_server.mylogger import logger
|
||||
|
||||
@@ -56,9 +54,9 @@ 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, yield_timeout: float = 0.0) -> \
|
||||
Generator[T, None, None]:
|
||||
"""Subscribes to events as an generator that yields events and automatically unsubscribes"""
|
||||
def subscribe(self, include_buffered: bool = True, include_future: bool = True,
|
||||
yield_timeout: float | None = 0.0) -> Generator[T, None, None]:
|
||||
"""Subscribes to events as a generator that yields events and automatically unsubscribes"""
|
||||
q = self._subscribe(include_buffered, include_future)
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -1,54 +1,64 @@
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCP.GCPnts import GCPnts_TangentialDeflection
|
||||
from OCP.BRepLib import BRepLib_ToolTriangulatedShape
|
||||
from OCP.TopAbs import TopAbs_Orientation
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
||||
from build123d import Shape, Vertex, Face, Location
|
||||
from build123d import Vertex, Face, Location, Compound, Vector
|
||||
from pygltflib import GLTF2
|
||||
|
||||
from yacv_server.cad import CADCoreLike
|
||||
from yacv_server.cad import CADCoreLike, ColorTuple
|
||||
from yacv_server.gltf import GLTFMgr
|
||||
from yacv_server.mylogger import logger
|
||||
|
||||
|
||||
def tessellate(
|
||||
cad_like: CADCoreLike,
|
||||
tolerance: float = 0.1,
|
||||
angular_tolerance: float = 0.1,
|
||||
faces: bool = True,
|
||||
edges: bool = True,
|
||||
vertices: bool = True,
|
||||
cad_like: CADCoreLike, color_faces: ColorTuple, color_edges: ColorTuple, color_vertices: ColorTuple,
|
||||
color_obj: Optional[ColorTuple] = None, tolerance: float = 0.1, angular_tolerance: float = 0.1,
|
||||
faces: bool = True, edges: bool = True, vertices: bool = True, texture: Optional[Tuple[bytes, str]] = None,
|
||||
) -> GLTF2:
|
||||
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
|
||||
mgr = GLTFMgr()
|
||||
if texture is None:
|
||||
mgr = GLTFMgr()
|
||||
else:
|
||||
mgr = GLTFMgr(texture)
|
||||
|
||||
if isinstance(cad_like, TopLoc_Location):
|
||||
mgr.add_location(Location(cad_like))
|
||||
|
||||
elif isinstance(cad_like, TopoDS_Shape):
|
||||
shape = Shape(cad_like)
|
||||
shape = Compound(cad_like)
|
||||
|
||||
# Perform tessellation tasks
|
||||
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)
|
||||
if faces and hasattr(shape, 'faces'):
|
||||
shape_faces = shape.faces()
|
||||
for face in shape_faces:
|
||||
_tessellate_face(mgr, face.wrapped, color_obj or color_faces, tolerance, angular_tolerance)
|
||||
if edges:
|
||||
for edge in face.edges():
|
||||
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
|
||||
if vertices:
|
||||
for vertex in face.vertices():
|
||||
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped]
|
||||
if edges:
|
||||
for edge in shape.edges():
|
||||
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance,
|
||||
angular_tolerance)
|
||||
if vertices:
|
||||
if len(shape_faces) > 0: color_obj = None # Don't color edges/vertices if faces are colored
|
||||
if edges and hasattr(shape, 'edges'):
|
||||
shape_edges = shape.edges()
|
||||
for edge in shape_edges:
|
||||
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), color_obj or color_edges,
|
||||
angular_tolerance, angular_tolerance)
|
||||
if len(shape_edges) > 0: color_obj = None # Don't color vertices if edges are colored
|
||||
if vertices and hasattr(shape, 'vertices'):
|
||||
for vertex in shape.vertices():
|
||||
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
|
||||
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []),
|
||||
color_obj or color_vertices)
|
||||
|
||||
else:
|
||||
raise TypeError(f"Unsupported type: {type(cad_like)}: {cad_like}")
|
||||
|
||||
return mgr.build()
|
||||
|
||||
@@ -56,17 +66,27 @@ def tessellate(
|
||||
def _tessellate_face(
|
||||
mgr: GLTFMgr,
|
||||
ocp_face: TopoDS_Face,
|
||||
color: ColorTuple,
|
||||
tolerance: float = 1e-3,
|
||||
angular_tolerance: float = 0.1
|
||||
angular_tolerance: float = 0.1,
|
||||
):
|
||||
face = Shape(ocp_face)
|
||||
face = Compound(ocp_face)
|
||||
# face.mesh(tolerance, angular_tolerance)
|
||||
tri_mesh = face.tessellate(tolerance, angular_tolerance)
|
||||
# noinspection PyArgumentList
|
||||
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
|
||||
if poly is None:
|
||||
logger.warn("No triangulation found for face")
|
||||
return GLTF2()
|
||||
|
||||
# Get the normal for each vertex (for smooth instead of flat shading!)
|
||||
BRepLib_ToolTriangulatedShape.ComputeNormals_s(face.wrapped, poly)
|
||||
reversed_face = face.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
|
||||
normals = [
|
||||
-Vector(v) if reversed_face else Vector(v)
|
||||
for v in (poly.Normal(i) for i in range(1, poly.NbNodes() + 1))
|
||||
]
|
||||
|
||||
# Get UV of each face from the parameters
|
||||
uv = [
|
||||
(v.X(), v.Y())
|
||||
@@ -75,7 +95,8 @@ def _tessellate_face(
|
||||
|
||||
vertices = tri_mesh[0]
|
||||
indices = tri_mesh[1]
|
||||
mgr.add_face(vertices, indices, uv)
|
||||
mgr.add_face(vertices, normals, indices, uv, color)
|
||||
return None
|
||||
|
||||
|
||||
def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tuple[float, float, float]:
|
||||
@@ -98,6 +119,7 @@ def _tessellate_edge(
|
||||
mgr: GLTFMgr,
|
||||
ocp_edge: TopoDS_Edge,
|
||||
faces: List[TopoDS_Face],
|
||||
color: ColorTuple,
|
||||
angular_deflection: float = 0.1,
|
||||
curvature_deflection: float = 0.1,
|
||||
):
|
||||
@@ -117,11 +139,9 @@ def _tessellate_edge(
|
||||
|
||||
# Convert strip of vertices to a list of pairs of vertices
|
||||
vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)]
|
||||
mgr.add_edge(vertices)
|
||||
mgr.add_edge(vertices, color)
|
||||
|
||||
|
||||
def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face]):
|
||||
def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], color: ColorTuple):
|
||||
c = Vertex(ocp_vertex).center()
|
||||
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
|
||||
|
||||
|
||||
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import atexit
|
||||
import base64
|
||||
import copy
|
||||
import inspect
|
||||
import os
|
||||
@@ -7,24 +8,27 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from http.server import ThreadingHTTPServer
|
||||
from importlib.metadata import version
|
||||
from io import BytesIO
|
||||
from threading import Thread
|
||||
from typing import Optional, Dict, Union, Callable, List, Tuple
|
||||
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
from PIL import Image
|
||||
# noinspection PyProtectedMember
|
||||
from build123d import Shape, Axis, Location, Vector
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
from yacv_server.cad import _hashcode, get_color, ColorTuple
|
||||
from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
|
||||
from yacv_server.gltf import get_version
|
||||
from yacv_server.myhttp import HTTPHandler
|
||||
from yacv_server.mylogger import logger
|
||||
from yacv_server.pubsub import BufferedPubSub
|
||||
from yacv_server.rwlock import RWLock
|
||||
from yacv_server.tessellate import tessellate
|
||||
from yacv_server.cad import _hashcode
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@@ -44,7 +48,7 @@ YACVSupported = Union[bytes, CADCoreLike]
|
||||
|
||||
class UpdatesApiFullData(UpdatesApiData):
|
||||
obj: YACVSupported
|
||||
"""The OCCT object, if any (not serialized)"""
|
||||
"""The OCCT object (not serialized)"""
|
||||
kwargs: Optional[Dict[str, any]]
|
||||
"""The show_object options, if any (not serialized)"""
|
||||
|
||||
@@ -61,10 +65,20 @@ class UpdatesApiFullData(UpdatesApiData):
|
||||
return super().to_json()
|
||||
|
||||
|
||||
class YACVProtocol(Enum):
|
||||
"""Enum of communication protocols supported by the server"""
|
||||
HTTP = auto()
|
||||
"""The recommended protocol for any platform that can run a web server."""
|
||||
STDERR = auto()
|
||||
"""Prints the updates one by one to stderr (first metadata, then base64 of glb file) using a special prefix. Required for Pyodide support."""
|
||||
|
||||
|
||||
class YACV:
|
||||
"""The main yacv_server class, which manages the web server and the CAD objects."""
|
||||
|
||||
# Startup
|
||||
protocol: YACVProtocol
|
||||
"""The protocol used by the server. Defaults to HTTP, but can be set to STDERR for Pyodide support."""
|
||||
server_thread: Optional[Thread]
|
||||
"""The main thread running the server (will spawn other threads for each request)"""
|
||||
server: Optional[ThreadingHTTPServer]
|
||||
@@ -88,7 +102,45 @@ class YACV:
|
||||
frontend_lock: RWLock
|
||||
"""Lock to ensure that the frontend has finished working before we shut down"""
|
||||
|
||||
texture: Optional[Tuple[bytes, str]]
|
||||
"""Default texture to use for model faces, in (data, mimetype) format.
|
||||
If left as None, no texture will be used.
|
||||
|
||||
It can be set with the YACV_TEXTURE=<uri> and overridden by the custom `yacv_texture` attribute of an object.
|
||||
The <uri> can be file:<path> or data:<mime>;base64,<data> where <mime> is the mime type and
|
||||
<data> is the base64 encoded image."""
|
||||
|
||||
color_faces: Optional[ColorTuple]
|
||||
"""Overrides the default color to use for model faces. Applies even if a texture is used.
|
||||
|
||||
You can use `show(..., color_faces=...)` or the standard way of setting colors for build123d/cadquery objects to
|
||||
override this color.
|
||||
|
||||
It can be set with the YACV_COLOR_FACES=<color> environment variable, where <color> is a color
|
||||
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
|
||||
|
||||
color_edges: Optional[ColorTuple]
|
||||
"""Overrides the default color to use for model edges.
|
||||
|
||||
You can use `show(..., color_edges=...) or the standard way of setting colors for build123d/cadquery objects to
|
||||
override this color.
|
||||
|
||||
It can be set with the YACV_COLOR_EDGES=<color> environment variable, where <color> is a color
|
||||
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
|
||||
|
||||
color_vertices: Optional[ColorTuple]
|
||||
"""Overrides the default color to use for model vertices.
|
||||
|
||||
You can use `show(..., color_vertices=...)` or the standard way of setting colors for build123d/cadquery objects to
|
||||
override this color.
|
||||
|
||||
It can be set with the YACV_COLOR_VERTICES=<color> environment variable, where <color> is a color
|
||||
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the YACV server"""
|
||||
raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper()
|
||||
self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP
|
||||
self.server_thread = None
|
||||
self.server = None
|
||||
self.startup_complete = threading.Event()
|
||||
@@ -98,10 +150,15 @@ class YACV:
|
||||
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'))
|
||||
self.texture = _read_texture_uri(os.getenv("YACV_TEXTURE"))
|
||||
self.color_faces = _read_color(os.getenv("YACV_COLOR_FACES", "#ffbf00")) # Default yellow
|
||||
self.color_edges = _read_color(os.getenv("YACV_COLOR_EDGES", "#1a1aff")) # Default blue
|
||||
self.color_vertices = _read_color(os.getenv("YACV_COLOR_VERTICES", "#1a1a1a")) # Default dark gray
|
||||
logger.info('Using yacv-server v%s', get_version())
|
||||
|
||||
def start(self):
|
||||
"""Starts the web server in the background"""
|
||||
if self.protocol == YACVProtocol.STDERR: return # No server to start, just print to stderr
|
||||
assert self.server_thread is None, "Server currently running, cannot start another one"
|
||||
assert self.startup_complete.is_set() is False, "Server already started"
|
||||
# Start the server in a separate daemon thread
|
||||
@@ -118,6 +175,8 @@ class YACV:
|
||||
# noinspection PyUnusedLocal
|
||||
def stop(self, *args):
|
||||
"""Stops the web server"""
|
||||
if self.protocol == YACVProtocol.STDERR: return # No server to stop, just print to stderr
|
||||
# The remainder is for the HTTP protocol only
|
||||
if self.server_thread is None:
|
||||
logger.error('Cannot stop server because it is not running')
|
||||
return
|
||||
@@ -125,7 +184,7 @@ class YACV:
|
||||
# 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))
|
||||
self._show_event(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))
|
||||
@@ -153,9 +212,11 @@ class YACV:
|
||||
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
|
||||
sys.exit(0) # Exit with success
|
||||
|
||||
_yacvServerModelPrefix = "yacv_server://model/"
|
||||
|
||||
def _run_server(self):
|
||||
"""Runs the web server"""
|
||||
logger.info('Starting server...')
|
||||
logger.info('Starting server in %s mode...', self.protocol.name)
|
||||
self.server = ThreadingHTTPServer(
|
||||
(os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))),
|
||||
lambda a, b, c: HTTPHandler(a, b, c, yacv=self))
|
||||
@@ -164,13 +225,50 @@ class YACV:
|
||||
self.startup_complete.set()
|
||||
self.server.serve_forever()
|
||||
|
||||
def _show_event(self, event: UpdatesApiFullData):
|
||||
"""Handles a show event by publishing it to the show events buffer (and special handling for stderr protocol)."""
|
||||
self.show_events.publish(event)
|
||||
# If the protocol is STDERR, we need to print the event to stderr
|
||||
if self.protocol == YACVProtocol.STDERR:
|
||||
msg = f'{self._yacvServerModelPrefix}{event.to_json()}'
|
||||
if not event.is_remove:
|
||||
# Always build the object even if the interface already has it (optimization disabled for Pyodide)
|
||||
glb_and_hash = self.export(event.name)
|
||||
if glb_and_hash is None:
|
||||
logger.warning('Object %s not found, ignoring it...', event.name)
|
||||
return
|
||||
glb = glb_and_hash[0]
|
||||
msg += f'{base64.b64encode(glb).decode("utf-8")}'
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
|
||||
def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]] = None, **kwargs):
|
||||
"""
|
||||
Shows the given CAD objects in the frontend. The objects will be tessellated and converted to GLTF. Optionally,
|
||||
the following keyword arguments can be used:
|
||||
|
||||
- auto_clear: Whether to clear the previous objects before showing the new ones (default: True)
|
||||
- texture: The texture to use for the faces of the object (see `YACV.texture` for more info)
|
||||
- color: The default color to use for the objects (can be overridden by the `color` attribute of each object)
|
||||
- tolerance: The tolerance for tessellating the object (default: 0.1)
|
||||
- angular_tolerance: The angular tolerance for tessellating the object (default: 0.1)
|
||||
- faces: Whether to tessellate and show the faces of the object (default: True)
|
||||
- edges: Whether to tessellate and show the edges of the object (default: True)
|
||||
- vertices: Whether to tessellate and show the vertices of the object (default: True)
|
||||
|
||||
:param objs: The CAD objects to show. Can be CAD-like objects (solids, locations, etc.) or bytes (GLTF) objects.
|
||||
:param names: The names of the objects. If None, the variable names will be used (if possible). The number of
|
||||
names must match the number of objects. An object of the same name will be replaced in the frontend.
|
||||
:param kwargs: Additional options for the show_object event.
|
||||
"""
|
||||
# Prepare the arguments
|
||||
start = time.time()
|
||||
names = names or [_find_var_name(obj) for obj in objs]
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
assert len(names) == len(objs), 'Number of names must match the number of objects'
|
||||
for color_name in ('color_faces', 'color_edges', 'color_vertices'):
|
||||
if color_name in kwargs:
|
||||
kwargs[color_name] = get_color(kwargs[color_name]) or _read_color(kwargs[color_name])
|
||||
|
||||
# Handle auto clearing of previous objects
|
||||
if kwargs.get('auto_clear', True):
|
||||
@@ -185,16 +283,22 @@ class YACV:
|
||||
|
||||
# Publish the show event
|
||||
for obj, name in zip(objs, names):
|
||||
obj_color = get_color(obj)
|
||||
# Some properties may be lost in preprocessing, so save them in kwargs
|
||||
_kwargs = kwargs.copy()
|
||||
if obj_color is not None:
|
||||
_kwargs['color_obj'] = obj_color # Only applies to highest-dimensional objects
|
||||
_kwargs['texture'] = _read_texture_uri(getattr(obj, 'yacv_texture', None) or kwargs.get('texture', None))
|
||||
if not isinstance(obj, bytes):
|
||||
obj = _preprocess_cad(obj, **kwargs)
|
||||
_hash = _hashcode(obj, **kwargs)
|
||||
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {})
|
||||
self.show_events.publish(event)
|
||||
obj = _preprocess_cad(obj, **_kwargs)
|
||||
_hash = _hashcode(obj, **_kwargs)
|
||||
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=_kwargs or {})
|
||||
self._show_event(event)
|
||||
|
||||
logger.info('show %s took %.3f seconds', names, time.time() - start)
|
||||
|
||||
def show_cad_all(self, **kwargs):
|
||||
"""Publishes all CAD objects in the current scope to the server"""
|
||||
"""Publishes all CAD objects in the current scope to the server. See `show` for more details."""
|
||||
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)
|
||||
|
||||
@@ -214,7 +318,7 @@ class YACV:
|
||||
# Publish the remove event
|
||||
show_event = copy.copy(show_events[-1])
|
||||
show_event.is_remove = True
|
||||
self.show_events.publish(show_event)
|
||||
self._show_event(show_event)
|
||||
|
||||
def clear(self, except_names: List[str] = None):
|
||||
"""Clears all previously-shown objects from the scene"""
|
||||
@@ -273,11 +377,17 @@ class YACV:
|
||||
if isinstance(event.obj, bytes): # Already a GLTF
|
||||
publish_to.publish(event.obj)
|
||||
else: # CAD object to tessellate and convert to GLTF
|
||||
gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1),
|
||||
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
|
||||
faces=event.kwargs.get('faces', True),
|
||||
edges=event.kwargs.get('edges', True),
|
||||
vertices=event.kwargs.get('vertices', True))
|
||||
gltf = tessellate(
|
||||
event.obj,
|
||||
color_faces=event.kwargs.get('color_faces', self.color_faces),
|
||||
color_edges=event.kwargs.get('color_edges', self.color_edges),
|
||||
color_vertices=event.kwargs.get('color_vertices', self.color_vertices),
|
||||
color_obj=event.kwargs.get('color_obj', None),
|
||||
tolerance=event.kwargs.get('tolerance', 0.1),
|
||||
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
|
||||
faces=event.kwargs.get('faces', True), edges=event.kwargs.get('edges', True),
|
||||
vertices=event.kwargs.get('vertices', True),
|
||||
texture=event.kwargs.get('texture', self.texture))
|
||||
glb_list_of_bytes = gltf.save_to_bytes()
|
||||
glb_bytes = b''.join(glb_list_of_bytes)
|
||||
publish_to.publish(glb_bytes)
|
||||
@@ -289,6 +399,7 @@ class YACV:
|
||||
try:
|
||||
return next(subscription), event.hash
|
||||
finally:
|
||||
# noinspection PyInconsistentReturns
|
||||
subscription.close()
|
||||
|
||||
def export_all(self, folder: str,
|
||||
@@ -301,6 +412,39 @@ class YACV:
|
||||
f.write(self.export(name)[0])
|
||||
|
||||
|
||||
def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
|
||||
if uri is None:
|
||||
return None
|
||||
if uri.startswith("file:"):
|
||||
path = uri[len("file:"):]
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
buf = BytesIO(data)
|
||||
img = Image.open(buf)
|
||||
mtype = img.get_format_mimetype()
|
||||
return data, mtype
|
||||
if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited)
|
||||
mtype_and_data = uri[len("data:"):]
|
||||
mtype = mtype_and_data.split(";", 1)[0]
|
||||
data_str = mtype_and_data.split(",", 1)[1]
|
||||
data = base64.b64decode(data_str)
|
||||
return data, mtype
|
||||
return None
|
||||
|
||||
|
||||
def _read_color(color: str) -> Optional[ColorTuple]:
|
||||
"""Reads a color from a string in the format #RRGGBB or #RRGGBBAA"""
|
||||
if color is None:
|
||||
return None
|
||||
if not color.startswith('#') or len(color) not in (7, 9):
|
||||
raise ValueError(f'Invalid color format: {color}')
|
||||
r = float(int(color[1:3], 16)) / 255.0
|
||||
g = float(int(color[3:5], 16)) / 255.0
|
||||
b = float(int(color[5:7], 16)) / 255.0
|
||||
a = float(int(color[7:9], 16)) / 255.0 if len(color) == 9 else 1.0
|
||||
return r, g, b, a
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
|
||||
# Get the shape of a CAD-like object
|
||||
@@ -320,19 +464,31 @@ def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
|
||||
return obj
|
||||
|
||||
|
||||
_find_var_name_count = 0
|
||||
_obj_name_counts = {}
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Build123d objects have a "label" property, CadQuery Assembly's have "name"
|
||||
for f in ('label', 'name'):
|
||||
if hasattr(obj, f):
|
||||
v = getattr(obj, f)
|
||||
if v != '':
|
||||
return v;
|
||||
|
||||
# Otherwise walk up our stack to see if there's a local variable that points to it
|
||||
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 get_shape(value, error=False) is obj_shape:
|
||||
return key
|
||||
_find_var_name_count += 1
|
||||
return 'unknown_var_' + str(_find_var_name_count)
|
||||
|
||||
# Last resort, name it for its type with a disambiguating number
|
||||
global _obj_name_counts
|
||||
t = obj.__class__.__name__
|
||||
_obj_name_counts[t] = 1 if t not in _obj_name_counts else _obj_name_counts[t] + 1
|
||||
return t + str(_obj_name_counts[t])
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
|
||||
Reference in New Issue
Block a user