Compare commits

..

36 Commits

Author SHA1 Message Date
Yeicor
9ca53bca61 fix issue with helpers 2024-03-09 12:48:22 +01:00
Yeicor
f1b04db24a small improvement for export 2024-03-09 12:05:40 +01:00
Yeicor
a304536536 Merge remote-tracking branch 'origin/master' 2024-03-09 11:47:31 +01:00
Yeicor
8b3d08826d small fix for pip 2024-03-09 11:47:23 +01:00
dependabot[bot]
40f424dd20 Bump typescript from 5.3.3 to 5.4.2 (#12)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.3 to 5.4.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.3...v5.4.2)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 09:20:51 +00:00
dependabot[bot]
4ddc38f5c5 Bump vue-tsc from 2.0.5 to 2.0.6 (#11)
Bumps [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v2.0.6/packages/tsc)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 09:20:21 +00:00
dependabot[bot]
f26d3a65a0 Bump vuetify from 3.5.7 to 3.5.8 (#10)
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.5.7 to 3.5.8.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.5.8/packages/vuetify)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 09:20:02 +00:00
Yeicor
768603b4d3 increase default request timeout 2024-03-07 20:56:51 +01:00
Yeicor
3e3730a4a5 faster multi-object load, faster updates and better orthographic camera at different scales 2024-03-07 20:49:27 +01:00
Yeicor
753648e522 fix imports 2024-03-06 19:25:49 +01:00
Yeicor
986db75b24 cleaner readme 2024-03-05 21:14:03 +01:00
Yeicor
962eea2b27 ready for release 0.2.0 2024-03-05 21:12:16 +01:00
Yeicor
8749c708e2 quick fixes 4 2024-03-05 21:08:24 +01:00
Yeicor
9954939aa0 quick fixes 3 2024-03-05 21:04:08 +01:00
Yeicor
8b59a5978e Merge remote-tracking branch 'origin/master' 2024-03-05 21:02:35 +01:00
Yeicor
fef6a1349c quick fixes 2 2024-03-05 21:02:28 +01:00
dependabot[bot]
817264289f Bump vite from 5.1.4 to 5.1.5 (#8)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.4 to 5.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.5/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 20:02:05 +00:00
Yeicor
b5c4e44eba Merge remote-tracking branch 'origin/master' 2024-03-05 21:01:28 +01:00
Yeicor
ea4a3bdb06 quick fixes 2024-03-05 21:01:19 +01:00
dependabot[bot]
6b771b4375 Bump vue-tsc from 2.0.3 to 2.0.5 (#7)
Bumps [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) from 2.0.3 to 2.0.5.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v2.0.5/packages/tsc)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 19:59:32 +00:00
Yeicor
1cbd1987b3 fully working example and many fixes 2024-03-05 20:58:14 +01:00
Yeicor
37a1c5de1f improve README.md 2024-03-03 21:12:45 +01:00
Yeicor
d3fbe254cb improve release asset 2 2024-03-03 20:47:29 +01:00
Yeicor
2475a00622 improve release asset 2024-03-03 20:45:12 +01:00
Yeicor
4bd025e7d5 fix launching backend without a built frontend 2024-03-03 20:41:44 +01:00
Yeicor
94e316472a fix selection 2024-03-03 20:38:56 +01:00
Yeicor
258256912b optimize CI times 2024-03-03 20:34:52 +01:00
Yeicor
075682ff18 fix for frontend parsing settings 2024-03-03 20:30:03 +01:00
Yeicor
cb1088fe71 add autoupdate.yml 2024-03-03 20:23:45 +01:00
Yeicor
682f41d3f8 fix frontend to use relative urls 2 2024-03-03 20:22:08 +01:00
Yeicor
dad2b4471a fix frontend to use relative urls 2024-03-03 20:18:34 +01:00
Yeicor
1c58c3d554 improve deploy workflow 7 2024-03-03 20:14:16 +01:00
Yeicor
5ff6e58071 improve deploy workflow 6 2024-03-03 20:08:32 +01:00
Yeicor
032f37a64b improve deploy workflow 5 2024-03-03 20:03:56 +01:00
31 changed files with 502 additions and 252 deletions

View File

@@ -12,6 +12,12 @@ updates:
interval: "weekly" interval: "weekly"
day: "saturday" day: "saturday"
time: "09:00" time: "09:00"
- package-ecosystem: "pip"
directory: "/example"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/.github/workflows/" directory: "/.github/workflows/"
schedule: schedule:

35
.github/workflows/autoupdate.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
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@v1"
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 }}"

View File

@@ -1,5 +1,3 @@
name: "build"
on: on:
push: push:
branches: branches:
@@ -23,7 +21,7 @@ jobs:
- uses: "actions/upload-artifact@v4" - uses: "actions/upload-artifact@v4"
with: with:
name: "frontend" name: "frontend"
path: "./dist" path: "dist"
retention-days: 5 retention-days: 5
build-backend: build-backend:
@@ -31,35 +29,48 @@ jobs:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-node@v4"
with:
cache: "yarn"
- run: "pipx install poetry" - run: "pipx install poetry"
- uses: "actions/setup-python@v5" - uses: "actions/setup-python@v5"
with: with:
python-version: "3.11" python-version: "3.11"
cache: "poetry" cache: "poetry"
- run: "poetry install" - run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "poetry build" - run: "SKIP_BUILD_FRONTEND=true poetry build"
build-logo: build-logo:
name: "Build logo" name: "Build logo"
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-node@v4"
with:
cache: "yarn"
- run: "pipx install poetry" - run: "pipx install poetry"
- uses: "actions/setup-python@v5" - uses: "actions/setup-python@v5"
with: with:
python-version: "3.11" python-version: "3.11"
cache: "poetry" cache: "poetry"
- run: "poetry install" - run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "poetry run python yacv_server/logo.py" - run: "poetry run python yacv_server/logo.py"
- run: "cp assets/fox.glb assets/logo_build/fox.glb" - run: "cp assets/fox.glb assets/logo_build/fox.glb"
- uses: "actions/upload-artifact@v4" - uses: "actions/upload-artifact@v4"
with: with:
name: "logo" name: "logo"
path: "./assets/logo_build" path: "assets/logo_build"
retention-days: 5
build-example:
name: "Build example"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
cache: "poetry"
- run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "PYTHONPATH=yacv_server YACV_DISABLE_SERVER=true poetry run python example/object.py"
- run: "mv export/object.glb export/example.glb"
- uses: "actions/upload-artifact@v4"
with:
name: "example"
path: "export"
retention-days: 5 retention-days: 5

View File

@@ -1,39 +1,60 @@
name: "maybe deploy"
on: on:
push: push:
tags: tags:
- "v**" - "v**"
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions: permissions:
contents: "write" contents: "write"
pages: "write"
id-token: "write"
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs: jobs:
deploy: # TODO: Update versions automatically
concurrency: "ci-${{ github.ref }}" # Recommended if you intend to make multiple deployments in quick succession.
deploy-frontend:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
environment:
name: "github-pages"
url: "${{ steps.deployment.outputs.page_url }}"
steps: steps:
- uses: "dawidd6/action-download-artifact@v3" - uses: "dawidd6/action-download-artifact@v3"
with: with:
workflow: "build" workflow: "build.yml"
name: "frontend" name: "frontend"
path: "./public" path: "./public"
allow_forks: false
- uses: "dawidd6/action-download-artifact@v3" - uses: "dawidd6/action-download-artifact@v3"
with: with:
workflow: "build" workflow: "build.yml"
name: "logo" name: "logo"
path: "./public" path: "./public"
- run: "ls -l -R ./public" allow_forks: false
- uses: "JamesIves/github-pages-deploy-action@v4" - uses: "dawidd6/action-download-artifact@v3"
with: with:
folder: "public" workflow: "build.yml"
name: "example"
path: "./public"
allow_forks: false
- uses: "actions/configure-pages@v4"
- uses: "actions/upload-pages-artifact@v3"
with:
path: 'public'
- id: "deployment"
uses: "actions/deploy-pages@v4"
- run: 'zip -r frontend.zip public'
- uses: "svenstaro/upload-release-action@v2" - uses: "svenstaro/upload-release-action@v2"
with: with:
repo_token: "${{ secrets.GITHUB_TOKEN }}" repo_token: "${{ secrets.GITHUB_TOKEN }}"
file: "./public/*" file: "frontend.zip"
asset_name: "frontend"
tag: "${{ github.ref }}" tag: "${{ github.ref }}"
overwrite: true overwrite: true
file_glob: true
# TODO: deploy-backend

View File

@@ -13,32 +13,16 @@ in a web browser.
- View and interact with topological entities: faces, edges, vertices and locations. - View and interact with topological entities: faces, edges, vertices and locations.
- Control clipping planes and transparency of each model. - Control clipping planes and transparency of each model.
- Select any entity and measure bounding box size and distances. - Select any entity and measure bounding box size and distances.
- Fully-featured [static deployment](#static-deployment): just upload the viewer and models to your server. - Fully-featured static deployment: just upload the viewer and models to your server.
- [Live lazy updates](#live-updates) while editing the CAD model (using the `yacv-server` package). - Hot reloading while editing the CAD model (using the `yacv-server` package).
## Usage & demo ## Usage
The [logo](yacv_server/logo.py) also works as an example of how to use the viewer. The [example](example) is a fully working project that shows how to use the viewer.
### Live updates You can play with the latest
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
To see the live updates you will need to run the [yacv_server](yacv_server) and (or
open [the viewer](https://yeicor-3d.github.io/yet-another-cad-viewer/) with [without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
the `preloadModels=ws://<host>:32323/` query parameter (by default it already tries localhost).
Note that [yacv_server](yacv_server) also hosts the frontend at `http://localhost:32323/` if you have no access to the
internet.
### Static deployment
To deploy the viewer and models as a static website you can simply copy the latest build directory to your server.
To load models use the `preloadModels=...` query parameter in the URL.
It can be set multiple times to load multiple models.
Note that you can simply reuse the [main deployment](https://yeicor-3d.github.io/yet-another-cad-viewer/) and host only
your own models (linking them from the viewer with the `preloadModels` query parameter).
To see a working example of a static deployment you can check out
the [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/?preloadModels=base.glb&preloadModels=fox.glb&preloadModels=img.jpg.glb&preloadModels=location.glb).
![Demo](assets/screenshot.png) ![Demo](assets/screenshot.png)

View File

@@ -523,8 +523,8 @@ Apache License
The following npm packages may be included in this product: The following npm packages may be included in this product:
- b4a@1.6.6 - b4a@1.6.6
- bare-events@2.2.0 - bare-events@2.2.1
- bare-fs@2.1.5 - bare-fs@2.2.1
- bare-os@2.2.0 - bare-os@2.2.0
- bare-path@2.1.0 - bare-path@2.1.0
@@ -1290,7 +1290,7 @@ third-party archives.
The following npm package may be included in this product: The following npm package may be included in this product:
- typescript@5.3.3 - typescript@5.4.2
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -1430,7 +1430,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm package may be included in this product: The following npm package may be included in this product:
- @lit-labs/ssr-dom-shim@1.1.2 - @lit-labs/ssr-dom-shim@1.2.0
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -1556,7 +1556,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- @babel/parser@7.23.9 - @babel/parser@7.24.0
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -1736,7 +1736,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- node-abi@3.54.0 - node-abi@3.56.0
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -1826,7 +1826,7 @@ SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- @monogrid/gainmap-js@3.0.1 - @monogrid/gainmap-js@3.0.3
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -2378,7 +2378,7 @@ THE SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- prebuild-install@7.1.1 - prebuild-install@7.1.2
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -2408,7 +2408,7 @@ THE SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- vuetify@3.5.3 - vuetify@3.5.8
This package contains the following license and notice below: This package contains the following license and notice below:
@@ -2498,16 +2498,16 @@ THE SOFTWARE.
The following npm packages may be included in this product: The following npm packages may be included in this product:
- @vue/compiler-core@3.4.16 - @vue/compiler-core@3.4.21
- @vue/compiler-dom@3.4.16 - @vue/compiler-dom@3.4.21
- @vue/compiler-sfc@3.4.16 - @vue/compiler-sfc@3.4.21
- @vue/compiler-ssr@3.4.16 - @vue/compiler-ssr@3.4.21
- @vue/reactivity@3.4.16 - @vue/reactivity@3.4.21
- @vue/runtime-core@3.4.16 - @vue/runtime-core@3.4.21
- @vue/runtime-dom@3.4.16 - @vue/runtime-dom@3.4.21
- @vue/server-renderer@3.4.16 - @vue/server-renderer@3.4.21
- @vue/shared@3.4.16 - @vue/shared@3.4.21
- vue@3.4.16 - vue@3.4.21
These packages each contain the following license and notice below: These packages each contain the following license and notice below:
@@ -2538,7 +2538,7 @@ THE SOFTWARE.
The following npm packages may be included in this product: The following npm packages may be included in this product:
- fast-fifo@1.3.2 - fast-fifo@1.3.2
- streamx@2.16.0 - streamx@2.16.1
These packages each contain the following license and notice below: These packages each contain the following license and notice below:

Binary file not shown.

View File

@@ -2,6 +2,8 @@ import os
import subprocess import subprocess
if __name__ == "__main__": if __name__ == "__main__":
# When building the backend, make sure the frontend is built first # Building the frontend is optional
subprocess.run(['yarn', 'install'], check=True) if os.getenv('SKIP_BUILD_FRONTEND') is None and os.path.exists('package.json'):
subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], check=True) # 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)

2
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/venv/
/export/

41
example/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Quickstart of Yet Another CAD Viewer
## Installation
1. Download the contents of this folder.
2. Assuming you have a recent version of Python installed, install the required packages:
```bash
python -m venv venv
pip install -r requirements.txt
# Do this every time you change the terminal:
. venv/bin/activate
```
## Usage
### Development with hot-reloading
To start the viewer, open the [GitHub Pages link](https://yeicor-3d.github.io/yet-another-cad-viewer/) of the frontend.
It will try to connect to the server at `127.0.0.1:32323` by default (this can be changed with the `preload` query
parameter).
Running `python object.py` is enough to push the model to the viewer. However, the recommended way for developing with
minimal latency is to run in cell mode (#%%). This way, the slow imports are only done once, and the server keeps
running. After editing the file you can just re-run the cell with the `show_object` call to push the changes to
the viewer.
### Static final deployment
Once your model is complete, you may want to share it with others using the same viewer.
You can do so by exporting the model as a .glb file as a last step of your script.
This is already done in `object.py` if the environment variable `CI` is set.
Once you have the `object.glb` file, you can host it on any static file server and share the following link with others:
`https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=<link-to-object.glb>`
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

25
example/object.py Normal file
View File

@@ -0,0 +1,25 @@
import os
from build123d import * # Also works with cadquery objects!
# Optional: enable logging to see what's happening
import logging
logging.basicConfig(level=logging.DEBUG)
from yacv_server import show_object, export_all # Check out all show_* methods for more features!
# %%
# Create a simple object
with BuildPart() as obj:
Box(10, 10, 5)
Cylinder(4, 5, mode=Mode.SUBTRACT)
# Show it in the frontend
show_object(obj, 'object')
# %%
# If running on CI, export the object to a .glb file compatible with the frontend
if 'CI' in os.environ:
export_all('export')

2
example/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
build123d
yacv-server

View File

@@ -46,7 +46,7 @@ async function onModelRemoveRequest(name: string) {
let networkMgr = new NetworkManager(); let networkMgr = new NetworkManager();
networkMgr.addEventListener('update', (e) => onModelLoadRequest(e as NetworkUpdateEvent)); networkMgr.addEventListener('update', (e) => onModelLoadRequest(e as NetworkUpdateEvent));
// Start loading all configured models ASAP // Start loading all configured models ASAP
for (let model of settings.preloadModels) { for (let model of settings.preload) {
networkMgr.load(model); networkMgr.load(model);
} }

View File

@@ -6,7 +6,8 @@ import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components/
<v-container> <v-container>
<v-row justify="center" style="height: 100%"> <v-row justify="center" style="height: 100%">
<v-col align-self="center"> <v-col align-self="center">
<v-progress-circular indeterminate style="display: block; margin: 0 auto;"/> <v-progress-circular indeterminate style="display: block; margin: 0 auto;"></v-progress-circular>
<slot/>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>

View File

@@ -24,8 +24,10 @@ export class NetworkManager extends EventTarget {
* Updates will be emitted as "update" events, including the download URL and the model name. * Updates will be emitted as "update" events, including the download URL and the model name.
*/ */
async load(url: string) { async load(url: string) {
if (url.startsWith("ws://") || url.startsWith("wss://")) { if (url.startsWith("dev+")) {
this.monitorWebSocket(url); let baseUrl = new URL(url.slice(4));
baseUrl.searchParams.set("api_updates", "true");
await this.monitorDevServer(baseUrl);
} else { } else {
// Get the last part of the URL as the "name" of the model // Get the last part of the URL as the "name" of the model
let name = url.split("/").pop(); let name = url.split("/").pop();
@@ -38,28 +40,51 @@ export class NetworkManager extends EventTarget {
} }
} }
private monitorWebSocket(url: string) { private async monitorDevServer(url: URL) {
// WARNING: This will spam the console logs with failed requests when the server is down try {
let ws = new WebSocket(url); // WARNING: This will spam the console logs with failed requests when the server is down
ws.onmessage = (event) => { let response = await fetch(url.toString());
let data = JSON.parse(event.data); console.log("Monitoring", url.toString(), response);
console.debug("WebSocket message", data); if (response.status === 200) {
let urlObj = new URL(url); let lines = readLinesStreamings(response.body!.getReader());
urlObj.protocol = urlObj.protocol === "ws:" ? "http:" : "https:"; for await (let line of lines) {
urlObj.searchParams.set("api_object", data.name); if (!line || !line.startsWith("data:")) continue;
this.foundModel(data.name, data.hash, urlObj.toString()); let data = JSON.parse(line.slice(5));
}; console.debug("WebSocket message", data);
ws.onerror = () => ws.close(); let urlObj = new URL(url);
ws.onclose = () => setTimeout(() => this.monitorWebSocket(url), settings.monitorEveryMs); urlObj.searchParams.delete("api_updates");
let timeoutFaster = setTimeout(() => ws.close(), settings.monitorOpenTimeoutMs); urlObj.searchParams.set("api_object", data.name);
ws.onopen = () => clearTimeout(timeoutFaster); this.foundModel(data.name, data.hash, urlObj.toString());
}
}
} catch (e) { // Ignore errors (retry very soon)
}
setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs);
return;
} }
private foundModel(name: string, hash: string | null, url: string) { private foundModel(name: string, hash: string | null, url: string) {
let prevHash = this.knownObjectHashes[name]; let prevHash = this.knownObjectHashes[name];
// TODO: Detect and manage instances of the same object (same hash, different name)
if (!hash || hash !== prevHash) { if (!hash || hash !== prevHash) {
this.knownObjectHashes[name] = hash; this.knownObjectHashes[name] = hash;
this.dispatchEvent(new NetworkUpdateEvent(name, url)); this.dispatchEvent(new NetworkUpdateEvent(name, url));
} }
} }
}
async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) {
let decoder = new TextDecoder();
let buffer = new Uint8Array();
while (true) {
let {value, done} = await reader.read();
if (done || !value) break;
buffer = new Uint8Array([...buffer, ...value]);
let text = decoder.decode(buffer);
let lines = text.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
yield lines[i];
}
buffer = new Uint8Array([...buffer.slice(-1)]);
}
} }

View File

@@ -20,7 +20,7 @@ export class SceneMgr {
if (name !== extrasNameValueHelpers) { if (name !== extrasNameValueHelpers) {
// Reload the helpers to fit the new model // Reload the helpers to fit the new model
// TODO: Only reload the helpers after a few milliseconds of no more models being added/removed // TODO: Only reload the helpers after a few milliseconds of no more models being added/removed
document = await this.reloadHelpers(sceneUrl, document); await this.reloadHelpers(sceneUrl, document);
} else { } else {
// Display the final fully loaded model // Display the final fully loaded model
let displayStart = performance.now(); let displayStart = performance.now();

View File

@@ -1,6 +1,6 @@
// These are the default values for the settings, which are overridden below // These are the default values for the settings, which are overridden below
export const settings = { export const settings = {
preloadModels: [ preload: [
// @ts-ignore // @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href, // new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore // @ts-ignore
@@ -10,11 +10,11 @@ export const settings = {
// @ts-ignore // @ts-ignore
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href, // new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
// Websocket URLs automatically listen for new models from the python backend // Websocket URLs automatically listen for new models from the python backend
"ws://127.0.0.1:32323/" "dev+http://127.0.0.1:32323/"
], ],
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100, monitorEveryMs: 100,
monitorOpenTimeoutMs: 100, monitorOpenTimeoutMs: 1000,
// ModelViewer settings // ModelViewer settings
autoplay: true, autoplay: true,
arModes: 'webxr scene-viewer quick-look', arModes: 'webxr scene-viewer quick-look',
@@ -29,15 +29,19 @@ function parseSetting(name: string, value: string): any {
if (arrayElem) name = name.slice(0, -2); if (arrayElem) name = name.slice(0, -2);
let prevValue = (settings as any)[name]; let prevValue = (settings as any)[name];
if (prevValue === undefined) throw new Error(`Unknown setting: ${name}`); if (prevValue === undefined) throw new Error(`Unknown setting: ${name}`);
if (Array.isArray(prevValue) && !arrayElem) { if (Array.isArray(prevValue)) {
let toExtend = [] if (!arrayElem) {
if (!firstTimeNames.includes(name)) { let toExtend = []
firstTimeNames.push(name); if (!firstTimeNames.includes(name)) {
firstTimeNames.push(name);
} else {
toExtend = prevValue;
}
toExtend.push(parseSetting(name + ".0", value));
return toExtend;
} else { } else {
toExtend = prevValue; prevValue = prevValue[0];
} }
toExtend.push(parseSetting(name + ".0", value));
return toExtend;
} }
switch (typeof prevValue) { switch (typeof prevValue) {
case 'boolean': case 'boolean':
@@ -47,7 +51,7 @@ function parseSetting(name: string, value: string): any {
case 'string': case 'string':
return value; return value;
default: default:
throw new Error(`Unknown setting type: ${typeof prevValue}`); throw new Error(`Unknown setting type: ${typeof prevValue} -- ${prevValue}`);
} }
} }

View File

@@ -88,7 +88,7 @@ let selectionListener = (event: MouseEvent) => {
// Find all hit objects and select the wanted one based on the filter // Find all hit objects and select the wanted one based on the filter
const hits = raycaster.intersectObject(scene, true); const hits = raycaster.intersectObject(scene, true);
let hit = hits.find((hit: Intersection<Object3D>) => { let hit = hits.find((hit: Intersection<Object3D>) => {
if (!hit.object || !(hit.object as any).isMesh) return false; if (!hit.object) return false;
const kind = hit.object.type const kind = hit.object.type
let isFace = kind === 'Mesh' || kind === 'SkinnedMesh'; let isFace = kind === 'Mesh' || kind === 'SkinnedMesh';
let isEdge = kind === 'Line' || kind === 'LineSegments'; let isEdge = kind === 'Line' || kind === 'LineSegments';

View File

@@ -50,12 +50,14 @@ function syncOrthoCamera(force: boolean) {
let perspectiveCam: PerspectiveCamera = (scene as any).__perspectiveCamera; let perspectiveCam: PerspectiveCamera = (scene as any).__perspectiveCamera;
if (force || perspectiveCam && scene.camera != perspectiveCam) { if (force || perspectiveCam && scene.camera != perspectiveCam) {
// Get zoom level from perspective camera // Get zoom level from perspective camera
let dist = scene.getTarget().distanceToSquared(perspectiveCam.position); let lookAtCenter = scene.getTarget().clone().add(scene.target.position);
let w = scene.aspect * dist ** 1.1 / 4000; let perspectiveWidthAtCenter =
let h = dist ** 1.1 / 4000; 2 * Math.tan(perspectiveCam.fov * Math.PI / 180 / 2) * perspectiveCam.position.distanceTo(lookAtCenter);
let w = perspectiveWidthAtCenter;
let h = perspectiveWidthAtCenter / scene.aspect;
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far); (scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
scene.camera.position.copy(perspectiveCam.position); scene.camera.position.copy(perspectiveCam.position);
scene.camera.lookAt(scene.getTarget().clone().add(scene.target.position)); scene.camera.lookAt(lookAtCenter);
if (force) scene.queueRender() // Force rerender of model-viewer if (force) scene.queueRender() // Force rerender of model-viewer
requestAnimationFrame(() => syncOrthoCamera(false)); requestAnimationFrame(() => syncOrthoCamera(false));
} }

View File

@@ -1,12 +1,9 @@
<script lang="ts">
</script>
<script setup lang="ts"> <script setup lang="ts">
import {settings} from "../misc/settings"; import {settings} from "../misc/settings";
import {onMounted, inject, type Ref} from "vue"; import {inject, onMounted, type Ref, ref, watch} from "vue";
import {$scene, $renderer} from "@google/model-viewer/lib/model-viewer-base"; import {VList, VListItem} from "vuetify/lib/components/index.mjs";
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
import Loading from "../misc/Loading.vue"; import Loading from "../misc/Loading.vue";
import {ref, watch} from "vue";
import {ModelViewerElement} from '@google/model-viewer'; import {ModelViewerElement} from '@google/model-viewer';
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot"; import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
@@ -152,7 +149,13 @@ watch(disableTap, (value) => {
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
:environment-image="settings.background"> :environment-image="settings.background">
<slot></slot> <!-- Controls, annotations, etc. --> <slot></slot> <!-- Controls, annotations, etc. -->
<loading class="annotation initial-load-banner"></loading> <div class="annotation initial-load-banner">
Trying to load models from...
<v-list v-for="src in settings.preload" :key="src">
<v-list-item>{{ src }}</v-list-item>
</v-list>
<loading></loading>
</div>
</model-viewer> </model-viewer>
<!-- The SVG overlay for fake 3D lines attached to the model --> <!-- The SVG overlay for fake 3D lines attached to the model -->
@@ -202,4 +205,15 @@ watch(disableTap, (value) => {
height: 100dvh; height: 100dvh;
pointer-events: none; pointer-events: none;
} }
.initial-load-banner {
width: 300px;
margin: auto;
margin-top: 3em;
overflow: hidden;
}
.initial-load-banner .v-list-item {
overflow: hidden;
}
</style> </style>

View File

@@ -1,6 +1,6 @@
{ {
"name": "yet-another-cad-viewer", "name": "yet-another-cad-viewer",
"version": "0.1.0", "version": "0.4.3",
"description": "", "description": "",
"license": "MIT", "license": "MIT",
"author": "Yeicor <4929005+Yeicor@users.noreply.github.com>", "author": "Yeicor <4929005+Yeicor@users.noreply.github.com>",
@@ -23,11 +23,11 @@
"three": "^0.160.1", "three": "^0.160.1",
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo", "three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
"vue": "^3.4.21", "vue": "^3.4.21",
"vuetify": "^3.5.7" "vuetify": "^3.5.8"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node20": "^20.1.2", "@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.24", "@types/node": "^20.11.25",
"@types/three": "^0.160.0", "@types/three": "^0.160.0",
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -36,9 +36,9 @@
"commander": "^12.0.0", "commander": "^12.0.0",
"generate-license-file": "^3.0.1", "generate-license-file": "^3.0.1",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"terser": "^5.28.1", "terser": "^5.29.1",
"typescript": "~5.3.0", "typescript": "~5.4.2",
"vite": "^5.0.11", "vite": "^5.1.5",
"vue-tsc": "^2.0.3" "vue-tsc": "^2.0.6"
} }
} }

16
poetry.lock generated
View File

@@ -128,6 +128,20 @@ devtools = ">=0.6"
Pygments = ">=2.2.0" Pygments = ">=2.2.0"
watchfiles = ">=0.10" watchfiles = ">=0.10"
[[package]]
name = "aiohttp-sse"
version = "2.2.0"
description = "Server-sent events support for aiohttp."
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-sse-2.2.0.tar.gz", hash = "sha256:a48dd5774031d3f41a29e159ebdbb93e89c8f37c1e9e83e196296be51885a5c2"},
{file = "aiohttp_sse-2.2.0-py3-none-any.whl", hash = "sha256:339f9803bcf4682a2060e75548760d86abe4424a0d92ba66ff4985de3bd743dc"},
]
[package.dependencies]
aiohttp = ">=3.0"
[[package]] [[package]]
name = "aiosignal" name = "aiosignal"
version = "1.3.1" version = "1.3.1"
@@ -1685,4 +1699,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "b319fd1a01b16c61a1e5a3050a22e4898d8faea9ab85de6ae4bc82849b1a4025" content-hash = "734e26c5b174a1b5d0942e9b67d6fff679e1e7e06c5f2fd9978911befc9aec3c"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "yacv-server" name = "yacv-server"
version = "0.1.0" # TODO: Update automatically by CI on release (also for package.json!) version = "0.4.3" # TODO: Update automatically by CI on release (also for package.json!)
description = "Yet Another CAD Viewer (server)" description = "Yet Another CAD Viewer (server)"
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"] authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
license = "MIT" license = "MIT"
@@ -19,6 +19,7 @@ ocp-tessellate = "^2.0.6"
# Web # Web
aiohttp = "^3.9.3" aiohttp = "^3.9.3"
aiohttp-sse = "^2.2.0"
aiohttp-cors = "^0.7.0" aiohttp-cors = "^0.7.0"
aiohttp-devtools = "^1.1.2" aiohttp-devtools = "^1.1.2"

View File

@@ -6,6 +6,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: "",
plugins: [ plugins: [
vue({ vue({
template: { template: {

View File

@@ -3,9 +3,8 @@ import os
import time import time
from aiohttp import web from aiohttp import web
from build123d import Vector
from server import Server from yacv_server.server import Server
server = Server() server = Server()
"""The server instance. This is the main entry point to serve CAD objects and other data to the frontend.""" """The server instance. This is the main entry point to serve CAD objects and other data to the frontend."""
@@ -21,6 +20,7 @@ show = server.show
show_object = show show_object = show
show_image = server.show_image show_image = server.show_image
show_all = server.show_cad_all show_all = server.show_cad_all
export_all = server.export_all
def _get_app() -> web.Application: def _get_app() -> web.Application:

View File

@@ -7,7 +7,7 @@ from typing import Optional, Union, List, Tuple
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from gltf import GLTFMgr from yacv_server.gltf import GLTFMgr
CADLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and Locations for now CADLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and Locations for now

View File

@@ -103,9 +103,10 @@ class GLTFMgr:
indices_blob = indices.flatten().tobytes() indices_blob = indices.flatten().tobytes()
# Check that all vertices are referenced by the indices # Check that all vertices are referenced by the indices
assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}" # This can happen on broken faces like on some fonts
assert indices.min() == 0 # assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}"
assert np.unique(indices.flatten()).size == len(vertices) # assert indices.min() == 0, f"min({indices}) != 0"
# assert np.unique(indices.flatten()).size == len(vertices)
assert len(tex_coord) == 0 or tex_coord.ndim == 2 assert len(tex_coord) == 0 or tex_coord.ndim == 2
assert len(tex_coord) == 0 or tex_coord.shape[1] == 2 assert len(tex_coord) == 0 or tex_coord.shape[1] == 2

View File

@@ -2,7 +2,7 @@ import asyncio
from typing import List, TypeVar, \ from typing import List, TypeVar, \
Generic, AsyncGenerator Generic, AsyncGenerator
from mylogger import logger from yacv_server.mylogger import logger
T = TypeVar('T') T = TypeVar('T')
@@ -61,4 +61,8 @@ class BufferedPubSub(Generic[T]):
def buffer(self) -> List[T]: def buffer(self) -> List[T]:
"""Returns a shallow copy of the list of buffered events""" """Returns a shallow copy of the list of buffered events"""
return self._buffer[:] return self._buffer[:]
def delete(self, event: T):
"""Deletes an event from the buffer"""
self._buffer.remove(event)

View File

@@ -6,19 +6,20 @@ import sys
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from threading import Thread from threading import Thread
from typing import Optional, Dict, Union from typing import Optional, Dict, Union, Callable
import aiohttp_cors import aiohttp_cors
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from aiohttp import web from aiohttp import web
from aiohttp_sse import sse_response
from build123d import Shape, Axis, Location, Vector from build123d import Shape, Axis, Location, Vector
from dataclasses_json import dataclass_json from dataclasses_json import dataclass_json
from cad import get_shape, grab_all_cad, image_to_gltf from yacv_server.cad import get_shape, grab_all_cad, image_to_gltf, CADLike
from mylogger import logger from yacv_server.mylogger import logger
from pubsub import BufferedPubSub from yacv_server.pubsub import BufferedPubSub
from tessellate import _hashcode, tessellate from yacv_server.tessellate import _hashcode, tessellate
# Find the frontend folder (optional, but recommended) # Find the frontend folder (optional, but recommended)
FILE_DIR = os.path.dirname(__file__) FILE_DIR = os.path.dirname(__file__)
@@ -28,6 +29,7 @@ if not os.path.exists(FRONTEND_BASE_PATH):
FRONTEND_BASE_PATH = os.path.join(FILE_DIR, '..', 'dist') FRONTEND_BASE_PATH = os.path.join(FILE_DIR, '..', 'dist')
else: else:
logger.warning('Frontend not found at %s', FRONTEND_BASE_PATH) logger.warning('Frontend not found at %s', FRONTEND_BASE_PATH)
FRONTEND_BASE_PATH = None
# Define the API paths (also available at the root path for simplicity) # Define the API paths (also available at the root path for simplicity)
UPDATES_API_PATH = '/api/updates' UPDATES_API_PATH = '/api/updates'
@@ -45,12 +47,12 @@ class UpdatesApiData:
class UpdatesApiFullData(UpdatesApiData): class UpdatesApiFullData(UpdatesApiData):
obj: Optional[TopoDS_Shape] obj: Optional[CADLike]
"""The OCCT object, if any (not serialized)""" """The OCCT object, if any (not serialized)"""
kwargs: Optional[Dict[str, any]] kwargs: Optional[Dict[str, any]]
"""The show_object options, if any (not serialized)""" """The show_object options, if any (not serialized)"""
def __init__(self, name: str, hash: str, obj: Optional[TopoDS_Shape] = None, def __init__(self, name: str, hash: str, obj: Optional[CADLike] = None,
kwargs: Optional[Dict[str, any]] = None): kwargs: Optional[Dict[str, any]] = None):
self.name = name self.name = name
self.hash = hash self.hash = hash
@@ -58,6 +60,7 @@ class UpdatesApiFullData(UpdatesApiData):
self.kwargs = kwargs self.kwargs = kwargs
def to_json(self) -> str: def to_json(self) -> str:
# noinspection PyUnresolvedReferences
return super().to_json() return super().to_json()
@@ -70,10 +73,13 @@ class Server:
app = web.Application() app = web.Application()
runner: web.AppRunner runner: web.AppRunner
thread: Optional[Thread] = None thread: Optional[Thread] = None
startup_complete = asyncio.Event()
do_shutdown = asyncio.Event() do_shutdown = asyncio.Event()
at_least_one_client = asyncio.Event()
show_events = BufferedPubSub[UpdatesApiFullData]() show_events = BufferedPubSub[UpdatesApiFullData]()
object_events: Dict[str, BufferedPubSub[bytes]] = {} object_events: Dict[str, BufferedPubSub[bytes]] = {}
object_events_lock = asyncio.Lock() object_events_lock = asyncio.Lock()
frontend_lock = asyncio.Lock() # To avoid exiting too early while frontend makes requests
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# --- Routes --- # --- Routes ---
@@ -84,7 +90,8 @@ class Server:
self.app.router.add_get('/', self._entrypoint) self.app.router.add_get('/', self._entrypoint)
# - Static files from the frontend # - Static files from the frontend
self.app.router.add_get('/{path:(.*/|)}', _index_handler) # Any folder -> index.html self.app.router.add_get('/{path:(.*/|)}', _index_handler) # Any folder -> index.html
self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend') if FRONTEND_BASE_PATH is not None:
self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend')
# --- CORS --- # --- CORS ---
cors = aiohttp_cors.setup(self.app, defaults={ cors = aiohttp_cors.setup(self.app, defaults={
"*": aiohttp_cors.ResourceOptions( "*": aiohttp_cors.ResourceOptions(
@@ -106,6 +113,11 @@ class Server:
signal.signal(signal.SIGINT | signal.SIGTERM, self.stop) signal.signal(signal.SIGINT | signal.SIGTERM, self.stop)
atexit.register(self.stop) atexit.register(self.stop)
self.thread.start() self.thread.start()
logger.info('Server started (requested)...')
# Wait for the server to be ready before returning
while not self.startup_complete.is_set():
time.sleep(0.01)
logger.info('Server started (received)...')
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def stop(self, *args): def stop(self, *args):
@@ -113,10 +125,33 @@ class Server:
if self.thread is None: if self.thread is None:
print('Cannot stop server because it is not running') print('Cannot stop server because it is not running')
return return
# FIXME: Wait for at least one client to confirm ready before stopping in case we are too fast?
graceful_secs_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0))
graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_REQUEST', 5.0))
# Make sure we can hold the lock for more than 100ms (to avoid exiting too early)
logger.info('Stopping server (waiting for at least one frontend request first, cancel with CTRL+C)...')
start = time.time()
try:
while not self.at_least_one_client.is_set() and time.time() - start < graceful_secs_connect:
time.sleep(0.01)
except KeyboardInterrupt:
pass
logger.info('Stopping server (waiting for no more frontend requests)...')
start = time.time()
while time.time() - start < graceful_secs_request:
if self.frontend_lock.locked():
start = time.time()
time.sleep(0.01)
# Stop the server in the background
self.loop.call_soon_threadsafe(lambda *a: self.do_shutdown.set()) self.loop.call_soon_threadsafe(lambda *a: self.do_shutdown.set())
self.thread.join(timeout=12) logger.info('Stopping server (sent)...')
# Wait for the server to stop gracefully
self.thread.join(timeout=30)
self.thread = None self.thread = None
logger.info('Stopping server (confirmed)...')
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM): if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
sys.exit(0) # Exit with success sys.exit(0) # Exit with success
@@ -133,15 +168,18 @@ class Server:
await runner.setup() await runner.setup()
site = web.TCPSite(runner, os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))) site = web.TCPSite(runner, os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323)))
await site.start() await site.start()
# print(f'Server started at {site.name}') logger.info('Server started (sent)...')
self.startup_complete.set()
# Wait for a signal to stop the server while running # Wait for a signal to stop the server while running
await self.do_shutdown.wait() await self.do_shutdown.wait()
# print('Shutting down server...') logger.info('Stopping server (received)...')
await runner.cleanup() await runner.shutdown()
# await runner.cleanup() # Gets stuck?
logger.info('Stopping server (done)...')
async def _entrypoint(self, request: web.Request) -> web.StreamResponse: async def _entrypoint(self, request: web.Request) -> web.StreamResponse:
"""Main entrypoint to the server, which automatically serves the frontend/updates/objects""" """Main entrypoint to the server, which automatically serves the frontend/updates/objects"""
if request.headers.get('Upgrade', '').lower() == 'websocket': # WebSocket -> updates API if request.query.get('api_updates', '') != '': # ?api_updates -> updates API
return await self._api_updates(request) return await self._api_updates(request)
elif request.query.get('api_object', '') != '': # ?api_object={name} -> object API elif request.query.get('api_object', '') != '': # ?api_object={name} -> object API
request.match_info['name'] = request.query['api_object'] request.match_info['name'] = request.query['api_object']
@@ -149,49 +187,51 @@ class Server:
else: # Anything else -> frontend index.html else: # Anything else -> frontend index.html
return await _index_handler(request) return await _index_handler(request)
async def _api_updates(self, request: web.Request) -> web.WebSocketResponse: async def _api_updates(self, request: web.Request) -> web.StreamResponse:
"""Handles a publish-only websocket connection that send show_object events along with their hashes and URLs""" """Handles a publish-only websocket connection that send show_object events along with their hashes and URLs"""
ws = web.WebSocketResponse() self.at_least_one_client.set()
await ws.prepare(request) async with sse_response(request) as resp:
logger.debug('Client connected: %s', request.remote)
async def _send_api_updates(): # Send buffered events first, while keeping a lock
subscription = self.show_events.subscribe() async with self.frontend_lock:
for data in self.show_events.buffer():
logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data)
# noinspection PyUnresolvedReferences
await resp.send(data.to_json())
# Send future events over the same connection
subscription = self.show_events.subscribe(include_buffered=False)
try: try:
async for data in subscription: async for data in subscription:
logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data) logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
await ws.send_str(data.to_json()) await resp.send(data.to_json())
finally: finally:
await subscription.aclose() await subscription.aclose()
logger.debug('Client disconnected: %s', request.remote)
# Start sending updates to the client automatically return resp
send_task = asyncio.create_task(_send_api_updates())
receive_task = asyncio.create_task(ws.receive())
try:
logger.debug('Client connected: %s', request.remote)
# Wait for the client to close the connection (or send a message)
done, pending = await asyncio.wait([send_task, receive_task], return_when=asyncio.FIRST_COMPLETED)
# Make sure to stop sending updates to the client and close the connection
for task in pending:
task.cancel()
finally:
await ws.close()
logger.debug('Client disconnected: %s', request.remote)
return ws
obj_counter = 0 obj_counter = 0
def _show_common(self, name: Optional[str], hash: str, start: float, obj: Optional[TopoDS_Shape] = None, def _show_common(self, name: Optional[str], hash: str, start: float, obj: Optional[CADLike] = None,
kwargs=None): kwargs=None):
name = name or f'object_{self.obj_counter}' name = name or f'object_{self.obj_counter}'
self.obj_counter += 1 self.obj_counter += 1
# Remove a previous object with the same name
for old_event in self.show_events.buffer():
if old_event.name == name:
self.show_events.delete(old_event)
if name in self.object_events:
del self.object_events[name]
break
precomputed_info = UpdatesApiFullData(name=name, hash=hash, obj=obj, kwargs=kwargs or {}) precomputed_info = UpdatesApiFullData(name=name, hash=hash, obj=obj, kwargs=kwargs or {})
self.show_events.publish_nowait(precomputed_info) self.show_events.publish_nowait(precomputed_info)
logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start) logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start)
return precomputed_info return precomputed_info
def show(self, any_object: Union[bytes, TopoDS_Shape, any], name: Optional[str] = None, **kwargs): def show(self, any_object: Union[bytes, CADLike, any], name: Optional[str] = None, **kwargs):
"""Publishes "any" object to the server""" """Publishes "any" object to the server"""
if isinstance(any_object, bytes): if isinstance(any_object, bytes):
self.show_gltf(any_object, name, **kwargs) self.show_gltf(any_object, name, **kwargs)
@@ -217,7 +257,7 @@ class Server:
# Publish it like any other GLTF object # Publish it like any other GLTF object
self.show_gltf(gltf, name, **kwargs) self.show_gltf(gltf, name, **kwargs)
def show_cad(self, obj: Union[TopoDS_Shape, any], name: Optional[str] = None, **kwargs): def show_cad(self, obj: Union[CADLike, any], name: Optional[str] = None, **kwargs):
"""Publishes a CAD object to the server""" """Publishes a CAD object to the server"""
start = time.time() start = time.time()
@@ -243,35 +283,34 @@ class Server:
async def _api_object(self, request: web.Request) -> web.Response: async def _api_object(self, request: web.Request) -> web.Response:
"""Returns the object file with the matching name, building it if necessary.""" """Returns the object file with the matching name, building it if necessary."""
# Export the object (or fail if not found) async with self.frontend_lock:
exported_glb = await self.export(request.match_info['name']) # Export the object (or fail if not found)
exported_glb = await self.export(request.match_info['name'])
# Wrap the GLB in a response and return it # Wrap the GLB in a response and return it
response = web.Response(body=exported_glb) response = web.Response(body=exported_glb)
response.content_type = 'model/gltf-binary' response.content_type = 'model/gltf-binary'
response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"' response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"'
return response return response
def shown_object_names(self) -> list[str]: def shown_object_names(self) -> list[str]:
"""Returns the names of all objects that have been shown""" """Returns the names of all objects that have been shown"""
return list([obj.name for obj in self.show_events.buffer()]) return list([obj.name for obj in self.show_events.buffer()])
def _shown_object(self, name: str) -> Optional[UpdatesApiFullData]:
"""Returns the object with the given name, if it exists"""
for obj in self.show_events.buffer():
if obj.name == name:
return obj
return None
async def export(self, name: str) -> bytes: async def export(self, name: str) -> bytes:
"""Export the given previously-shown object to a single GLB file, building it if necessary.""" """Export the given previously-shown object to a single GLB file, building it if necessary."""
start = time.time() start = time.time()
# Check that the object to build exists and grab it if it does # Check that the object to build exists and grab it if it does
found = False event = self._shown_object(name)
obj: Optional[TopoDS_Shape] = None if not event:
kwargs: Optional[Dict[str, any]] = None
subscription = self.show_events.buffer()
for data in subscription:
if data.name == name:
obj = data.obj
kwargs = data.kwargs
found = True # Required because obj could be None
break
if not found:
raise web.HTTPNotFound(text=f'No object named {name} was previously shown') raise web.HTTPNotFound(text=f'No object named {name} was previously shown')
# Use the lock to ensure that we don't build the object twice # Use the lock to ensure that we don't build the object twice
@@ -284,23 +323,38 @@ class Server:
def _build_object(): def _build_object():
# Build and publish the object (once) # Build and publish the object (once)
gltf = tessellate(obj, tolerance=kwargs.get('tolerance', 0.1), gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1),
angular_tolerance=kwargs.get('angular_tolerance', 0.1), angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
faces=kwargs.get('faces', True), faces=event.kwargs.get('faces', True),
edges=kwargs.get('edges', True), edges=event.kwargs.get('edges', True),
vertices=kwargs.get('vertices', True)) vertices=event.kwargs.get('vertices', True))
glb_list_of_bytes = gltf.save_to_bytes() glb_list_of_bytes = gltf.save_to_bytes()
publish_to.publish_nowait(b''.join(glb_list_of_bytes)) publish_to.publish_nowait(b''.join(glb_list_of_bytes))
logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start, logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start,
len(gltf.meshes[0].primitives)) len(gltf.meshes[0].primitives))
# We should build it fully even if we are cancelled, so we use a separate task # await asyncio.get_running_loop().run_in_executor(None, _build_object)
# Furthermore, building is CPU-bound, so we use the default executor # The previous line has problems with auto-closed loop on script exit
await asyncio.get_running_loop().run_in_executor(None, _build_object) # and is cancellable, so instead run blocking code in async context :(
logger.debug('Building object %s... %s', name, event.obj)
_build_object()
# In either case return the elements of a subscription to the async generator # In either case return the elements of a subscription to the async generator
subscription = self.object_events[name].subscribe() subscription = self.object_events[name].subscribe()
try: try:
return await anext(subscription) return await anext(subscription)
finally: finally:
await subscription.aclose() await subscription.aclose()
def export_all(self, folder: str, export_filter: Callable[[str, Optional[CADLike]], bool] = lambda name, obj: True):
"""Export all previously-shown objects to GLB files in the given folder"""
import asyncio
async def _export_all():
os.makedirs(folder, exist_ok=True)
for name in self.shown_object_names():
if export_filter(name, self._shown_object(name).obj):
with open(os.path.join(folder, f'{name}.glb'), 'wb') as f:
f.write(await self.export(name))
asyncio.run(_export_all())

View File

@@ -13,9 +13,9 @@ from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
from build123d import Shape, Vertex, Face, Location from build123d import Shape, Vertex, Face, Location
from pygltflib import GLTF2 from pygltflib import GLTF2
import mylogger from yacv_server.cad import CADLike
from cad import CADLike from yacv_server.gltf import GLTFMgr
from gltf import GLTFMgr from yacv_server.mylogger import logger
def tessellate( def tessellate(
@@ -68,7 +68,7 @@ def _tessellate_face(
face.mesh(tolerance, angular_tolerance) face.mesh(tolerance, angular_tolerance)
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location()) poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
if poly is None: if poly is None:
mylogger.logger.warn("No triangulation found for face") logger.warn("No triangulation found for face")
return GLTF2() return GLTF2()
tri_mesh = face.tessellate(tolerance, angular_tolerance) tri_mesh = face.tessellate(tolerance, angular_tolerance)

View File

@@ -805,10 +805,10 @@
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23" resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg== integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
"@types/node@^20.11.24": "@types/node@^20.11.25":
version "20.11.24" version "20.11.25"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.25.tgz#0f50d62f274e54dd7a49f7704cc16bfbcccaf49f"
integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== integrity sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
@@ -851,26 +851,26 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37"
integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==
"@volar/language-core@2.1.0", "@volar/language-core@~2.1.0": "@volar/language-core@2.1.2", "@volar/language-core@~2.1.2":
version "2.1.0" version "2.1.2"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.0.tgz#26953a62f5d956a4ba4003faf59ae09b2a8aabb6" resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.2.tgz#2053c0ee48a822d5418be2c192e51e580764b49f"
integrity sha512-BrYEgYHx92ocpt1OUxJs2x3TAXEjpPLxsQoARb96g2GdF62xnfRQUqCNBwiU7Z3MQ/0tOAdqdHNYNmrFtx6q4A== integrity sha512-5qsDp0Gf6fE09UWCeK7bkVn6NxMwC9OqFWQkMMkeej8h8XjyABPdRygC2RCrqDrfVdGijqlMQeXs6yRS+vfZYA==
dependencies: dependencies:
"@volar/source-map" "2.1.0" "@volar/source-map" "2.1.2"
"@volar/source-map@2.1.0": "@volar/source-map@2.1.2":
version "2.1.0" version "2.1.2"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.0.tgz#f8c70b5043ae4a3d2cbd66a84036ef030b655a8e" resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.2.tgz#d270ff8ef5c814582f0efe08272c0fd6b9effb3b"
integrity sha512-VPyi+DTv67cvUOkUewzsOQJY3VUhjOjQxigT487z/H7tEI8ZFd5RksC5afk3JelOK+a/3Y8LRDbKmYKu1dz87g== integrity sha512-yFJqsuLm1OaWrsz9E3yd3bJcYIlHqdZ8MbmIoZLrAzMYQDcoF26/INIhgziEXSdyHc8xd7rd/tJdSnUyh0gH4Q==
dependencies: dependencies:
muggle-string "^0.4.0" muggle-string "^0.4.0"
"@volar/typescript@~2.1.0": "@volar/typescript@~2.1.2":
version "2.1.0" version "2.1.2"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.0.tgz#640abcdcb6b822f9860006d090e1d5252c655e37" resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.2.tgz#61f838cf4410e328a7ba638fadc41bb814772508"
integrity sha512-2cicVoW4q6eU/omqfOBv+6r9JdrF5bBelujbJhayPNKiOj/xwotSJ/DM8IeMvTZvtkOZkm6suyOCLEokLY0w2w== integrity sha512-lhTancZqamvaLvoz0u/uth8dpudENNt2LFZOWCw9JZiX14xRFhdhfzmphiCRb7am9E6qAJSbdS/gMt1utXAoHQ==
dependencies: dependencies:
"@volar/language-core" "2.1.0" "@volar/language-core" "2.1.2"
path-browserify "^1.0.1" path-browserify "^1.0.1"
"@vue/babel-helper-vue-transform-on@1.2.1": "@vue/babel-helper-vue-transform-on@1.2.1":
@@ -948,12 +948,12 @@
"@vue/compiler-dom" "3.4.21" "@vue/compiler-dom" "3.4.21"
"@vue/shared" "3.4.21" "@vue/shared" "3.4.21"
"@vue/language-core@2.0.3": "@vue/language-core@2.0.6":
version "2.0.3" version "2.0.6"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.3.tgz#49e290c928b216a5b0f07012ff6e1065a6e15258" resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.6.tgz#876f90622a3f801dce5cedcd6eae429d732152e2"
integrity sha512-hnVF/Q3cD2v+EFD4pD1YdITGBcdM38P18SYqilVQDezKw5RobWny4BwIckWGS1fJmUstsO9mTX30ZOyzyR2Q+Q== integrity sha512-UzqU12tzf9XLqRO3TiWPwRNpP4fyUzE6MAfOQWQNZ4jy6a30ARRUpmODDKq6O8C4goMc2AlPqTmjOHPjHkilSg==
dependencies: dependencies:
"@volar/language-core" "~2.1.0" "@volar/language-core" "~2.1.2"
"@vue/compiler-dom" "^3.4.0" "@vue/compiler-dom" "^3.4.0"
"@vue/shared" "^3.4.0" "@vue/shared" "^3.4.0"
computeds "^0.0.1" computeds "^0.0.1"
@@ -2878,10 +2878,10 @@ tar@^6.1.11, tar@^6.1.2:
mkdirp "^1.0.3" mkdirp "^1.0.3"
yallist "^4.0.0" yallist "^4.0.0"
terser@^5.28.1: terser@^5.29.1:
version "5.28.1" version "5.29.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.28.1.tgz#bf00f7537fd3a798c352c2d67d67d65c915d1b28" resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.1.tgz#44e58045b70c09792ba14bfb7b4e14ca8755b9fa"
integrity sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA== integrity sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==
dependencies: dependencies:
"@jridgewell/source-map" "^0.3.3" "@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2" acorn "^8.8.2"
@@ -2935,10 +2935,10 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
typescript@~5.3.0: typescript@~5.4.2:
version "5.3.3" version "5.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
undici-types@~5.26.4: undici-types@~5.26.4:
version "5.26.5" version "5.26.5"
@@ -2992,10 +2992,10 @@ validate-npm-package-name@^5.0.0:
dependencies: dependencies:
builtins "^5.0.0" builtins "^5.0.0"
vite@^5.0.11: vite@^5.1.5:
version "5.1.4" version "5.1.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.4.tgz#14e9d3e7a6e488f36284ef13cebe149f060bcfb6" resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.5.tgz#bdbc2b15e8000d9cc5172f059201178f9c9de5fb"
integrity sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg== integrity sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==
dependencies: dependencies:
esbuild "^0.19.3" esbuild "^0.19.3"
postcss "^8.4.35" postcss "^8.4.35"
@@ -3011,13 +3011,13 @@ vue-template-compiler@^2.7.14:
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.2.0" he "^1.2.0"
vue-tsc@^2.0.3: vue-tsc@^2.0.6:
version "2.0.3" version "2.0.6"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.3.tgz#9b736f6ad478a5c98a23aeef509eb0b73d115b26" resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.6.tgz#29cb195ffea63d66ec70b64eb4aadc6cd162bb12"
integrity sha512-aMJqbgLiKDAwAglWqMoGf1Ez6Wwqhlk2MDxEjFGziiLW0A+tHOWE1+YQJZQ1Vm6zaENPA2KJAubFhaR988UvGg== integrity sha512-kK50W4XqQL34vHRkxlRWLicrT6+F9xfgCgJ4KSmCHcytKzc1u3c94XXgI+CjmhOSxyw0krpExF7Obo7y4+0dVQ==
dependencies: dependencies:
"@volar/typescript" "~2.1.0" "@volar/typescript" "~2.1.2"
"@vue/language-core" "2.0.3" "@vue/language-core" "2.0.6"
semver "^7.5.4" semver "^7.5.4"
vue@^3.4.21: vue@^3.4.21:
@@ -3031,10 +3031,10 @@ vue@^3.4.21:
"@vue/server-renderer" "3.4.21" "@vue/server-renderer" "3.4.21"
"@vue/shared" "3.4.21" "@vue/shared" "3.4.21"
vuetify@^3.5.7: vuetify@^3.5.8:
version "3.5.7" version "3.5.8"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.7.tgz#9dfa027a582aa7d2211c8019c33ef7cadd66c5c0" resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.8.tgz#bc8f08dfd3314640e7b5d43b50138a26d650cbbf"
integrity sha512-BFj/puY8odRwY50pRfE1gpawnxreY8PtPb/tDw3oumxSLXhoXw8q6YLA6QUvqZrYEzcYpojxZIYhNWUky2KN1w== integrity sha512-8nGS+lKejZkev55HFwIfsRt+9fOqbeDQNmXxfmLKAlnUT8FtynVwbjAwHMtX/OQAQ3ZwRaR1ptqQQmx3OgxzbQ==
walk-up-path@^3.0.1: walk-up-path@^3.0.1:
version "3.0.1" version "3.0.1"