Compare commits

...

24 Commits

Author SHA1 Message Date
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
Yeicor
dc12f83780 improve deploy workflow 4 2024-03-03 20:01:19 +01:00
Yeicor
079c7217b6 improve deploy workflow 3 2024-03-03 19:57:27 +01:00
25 changed files with 398 additions and 178 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_STOP_EARLY=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,57 +1,60 @@
name: "maybe deploy"
on: on:
workflow_run: push:
workflows: [ "build" ] tags:
types: [ "completed" ] - "v**"
branches: [ "master" ] 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:
check-early-exit: # TODO: Update versions automatically
deploy-frontend:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
outputs: environment:
should-deploy: "${{ steps.check-early-exit.outputs.should-deploy }}" name: "github-pages"
steps: url: "${{ steps.deployment.outputs.page_url }}"
- run: |
if [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then
echo "Not deploying because the CI workflow did not succeed."
echo "should-deploy=false" >> $GITHUB_ENV
elif ! echo "${{ github.ref }}" | grep -q -E "^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+.*"; then
echo "Not deploying because the CI workflow was not triggered by a release tag."
echo "should-deploy=false" >> $GITHUB_ENV
else
echo "should-deploy=true" >> $GITHUB_ENV
fi
deploy:
concurrency: "ci-${{ github.ref }}" # Recommended if you intend to make multiple deployments in quick succession.
runs-on: "ubuntu-latest"
needs: "check-early-exit"
if: "needs.check-early-exit.outputs.should-deploy == 'true'"
steps: steps:
- uses: "dawidd6/action-download-artifact@v3" - uses: "dawidd6/action-download-artifact@v3"
with: with:
workflow: "ci.yml" 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: "ci.yml" 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,18 +13,18 @@ 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](yacv_server)). - Hot reloading while editing the CAD model (using the `yacv-server` package).
## Usage & demo ## Usage
The latest build is available at https://yeicor-3d.github.io/yet-another-cad-viewer/. The [example](example) is a fully working project that demonstrates how to use the viewer.
### Live updates ### Hot reloading
To see the live updates you will need to run the [yacv_server](yacv_server) and To see the live updates you will need to run the [yacv_server](yacv_server) and
open [the viewer](https://yeicor-3d.github.io/yet-another-cad-viewer/) with open [the viewer](https://yeicor-3d.github.io/yet-another-cad-viewer/) with
the `preloadModels=ws://<host>:32323/` query parameter (by default it already tries localhost). the `preload=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 Note that [yacv_server](yacv_server) also hosts the frontend at `http://localhost:32323/` if you have no access to the
internet. internet.
@@ -32,13 +32,15 @@ internet.
### Static deployment ### Static deployment
To deploy the viewer and models as a static website you can simply copy the latest build directory to your server. 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. To load models use the `preload=...` query parameter in the URL.
It can be set multiple times to load multiple models. 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 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). your own models (linking them from the viewer with the `preload` query parameter).
To see a working example of a static deployment you can check out 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). the [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
(or
the [demo 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)).
![Demo](assets/screenshot.png) ![Demo](assets/screenshot.png)

Binary file not shown.

View File

@@ -2,6 +2,7 @@ import os
import subprocess import subprocess
if __name__ == "__main__": if __name__ == "__main__":
if os.getenv('SKIP_BUILD_FRONTEND') is None:
# When building the backend, make sure the frontend is built first # When building the backend, make sure the frontend is built first
subprocess.run(['yarn', 'install'], check=True) subprocess.run(['yarn', 'install'], check=True)
subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], 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");
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,21 +40,20 @@ export class NetworkManager extends EventTarget {
} }
} }
private monitorWebSocket(url: string) { private monitorDevServer(url: URL) {
// WARNING: This will spam the console logs with failed requests when the server is down // WARNING: This will spam the console logs with failed requests when the server is down
let ws = new WebSocket(url); let eventSource = new EventSource(url);
ws.onmessage = (event) => { eventSource.onmessage = (event) => {
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
console.debug("WebSocket message", data); console.debug("WebSocket message", data);
let urlObj = new URL(url); let urlObj = new URL(url);
urlObj.protocol = urlObj.protocol === "ws:" ? "http:" : "https:"; urlObj.searchParams.delete("api_updates");
urlObj.searchParams.set("api_object", data.name); urlObj.searchParams.set("api_object", data.name);
this.foundModel(data.name, data.hash, urlObj.toString()); this.foundModel(data.name, data.hash, urlObj.toString());
}; };
ws.onerror = () => ws.close(); eventSource.onerror = () => { // Retry after a very short delay
ws.onclose = () => setTimeout(() => this.monitorWebSocket(url), settings.monitorEveryMs); setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs);
let timeoutFaster = setTimeout(() => ws.close(), settings.monitorOpenTimeoutMs); }
ws.onopen = () => clearTimeout(timeoutFaster);
} }
private foundModel(name: string, hash: string | null, url: string) { private foundModel(name: string, hash: string | null, url: string) {

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,7 +29,8 @@ 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)) {
if (!arrayElem) {
let toExtend = [] let toExtend = []
if (!firstTimeNames.includes(name)) { if (!firstTimeNames.includes(name)) {
firstTimeNames.push(name); firstTimeNames.push(name);
@@ -38,6 +39,9 @@ function parseSetting(name: string, value: string): any {
} }
toExtend.push(parseSetting(name + ".0", value)); toExtend.push(parseSetting(name + ".0", value));
return toExtend; return toExtend;
} else {
prevValue = prevValue[0];
}
} }
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

@@ -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

@@ -38,7 +38,7 @@
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"terser": "^5.28.1", "terser": "^5.28.1",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"vite": "^5.0.11", "vite": "^5.1.5",
"vue-tsc": "^2.0.3" "vue-tsc": "^2.0.5"
} }
} }

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

@@ -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,7 +3,6 @@ import os
import time import time
from aiohttp import web from aiohttp import web
from build123d import Vector
from server import Server from server import Server
@@ -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

@@ -62,3 +62,7 @@ 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

@@ -12,6 +12,7 @@ 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
@@ -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'
@@ -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,6 +90,7 @@ 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
if FRONTEND_BASE_PATH is not None:
self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend') 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={
@@ -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,31 @@ 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?
if os.getenv('YACV_STOP_EARLY', '') == '':
# 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)...')
try:
while not self.at_least_one_client.is_set():
time.sleep(0.01)
except KeyboardInterrupt:
pass
logger.info('Stopping server (waiting for no more frontend requests)...')
acquired = time.time()
while time.time() - acquired < 1.0:
if self.frontend_lock.locked():
acquired = 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 +166,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,36 +185,32 @@ 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)
resp.ping_interval = 0.1 # HACK: forces flushing of the buffer
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()
# Start sending updates to the client automatically
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) logger.debug('Client disconnected: %s', request.remote)
return ws return resp
obj_counter = 0 obj_counter = 0
@@ -186,6 +218,13 @@ class Server:
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)
@@ -243,6 +282,7 @@ 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."""
async with self.frontend_lock:
# Export the object (or fail if not found) # Export the object (or fail if not found)
exported_glb = await self.export(request.match_info['name']) exported_glb = await self.export(request.match_info['name'])
@@ -256,22 +296,20 @@ class Server:
"""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,19 +322,21 @@ 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()
@@ -304,3 +344,15 @@ class Server:
return await anext(subscription) return await anext(subscription)
finally: finally:
await subscription.aclose() await subscription.aclose()
def export_all(self, folder: str) -> None:
"""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():
with open(os.path.join(folder, f'{name}.glb'), 'wb') as f:
f.write(await self.export(name))
asyncio.run(_export_all())

View File

@@ -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.1", "@volar/language-core@~2.1.1":
version "2.1.0" version "2.1.1"
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.1.tgz#ea7c2448ac5bdb2dd2ed202e5ff57929cb8ef191"
integrity sha512-BrYEgYHx92ocpt1OUxJs2x3TAXEjpPLxsQoARb96g2GdF62xnfRQUqCNBwiU7Z3MQ/0tOAdqdHNYNmrFtx6q4A== integrity sha512-oVbZcj97+5zlowkHMSJMt3aaAFuFyhXeXoOEHcqGECxFvw1TPCNnMM9vxhqNpoiNeWKHvggoq9WCk/HzJHtP8A==
dependencies: dependencies:
"@volar/source-map" "2.1.0" "@volar/source-map" "2.1.1"
"@volar/source-map@2.1.0": "@volar/source-map@2.1.1":
version "2.1.0" version "2.1.1"
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.1.tgz#9ca00177177417496a0364cea2f965445e19abb2"
integrity sha512-VPyi+DTv67cvUOkUewzsOQJY3VUhjOjQxigT487z/H7tEI8ZFd5RksC5afk3JelOK+a/3Y8LRDbKmYKu1dz87g== integrity sha512-OOtxrEWB2eZ+tnCy5JwDkcCPGlN3+ioNNzkywXE9k4XA7p4cN36frR7QPAOksvd7RXKUGHzSjq6XrYnTPa4z4Q==
dependencies: dependencies:
muggle-string "^0.4.0" muggle-string "^0.4.0"
"@volar/typescript@~2.1.0": "@volar/typescript@~2.1.1":
version "2.1.0" version "2.1.1"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.0.tgz#640abcdcb6b822f9860006d090e1d5252c655e37" resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.1.tgz#b3dddaf39140cc0e00d67bad943496e2470a3882"
integrity sha512-2cicVoW4q6eU/omqfOBv+6r9JdrF5bBelujbJhayPNKiOj/xwotSJ/DM8IeMvTZvtkOZkm6suyOCLEokLY0w2w== integrity sha512-5K41AWvFZCMMKZCx8bbFvbkyiKHr0s9k8P0M1FVXLX/9HYHzK5C9B8cX4uhATSehAytFIRnR4fTXVQtWp/Yzag==
dependencies: dependencies:
"@volar/language-core" "2.1.0" "@volar/language-core" "2.1.1"
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.5":
version "2.0.3" version "2.0.5"
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.5.tgz#bd3502604ea785f4171815005997988563f18469"
integrity sha512-hnVF/Q3cD2v+EFD4pD1YdITGBcdM38P18SYqilVQDezKw5RobWny4BwIckWGS1fJmUstsO9mTX30ZOyzyR2Q+Q== integrity sha512-knGXuQqhDSO7QJr8LFklsiWa23N2ikehkdVxtc9UKgnyqsnusughS2Tkg7VN8Hqed35X0B52Z+OGI5OrT/8uxQ==
dependencies: dependencies:
"@volar/language-core" "~2.1.0" "@volar/language-core" "~2.1.1"
"@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"
@@ -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.5:
version "2.0.3" version "2.0.5"
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.5.tgz#f491e24d74fcbf50cc3a71fce5ac2c99ee6335d9"
integrity sha512-aMJqbgLiKDAwAglWqMoGf1Ez6Wwqhlk2MDxEjFGziiLW0A+tHOWE1+YQJZQ1Vm6zaENPA2KJAubFhaR988UvGg== integrity sha512-e8WCgOVTrbmC04XPnI+IpaMTFYKaTm5s/MXFcvxO1l9kxzn+9FpGNVrBSlQE8VpTJaJg4kaBK1nj3NC20VJzjw==
dependencies: dependencies:
"@volar/typescript" "~2.1.0" "@volar/typescript" "~2.1.1"
"@vue/language-core" "2.0.3" "@vue/language-core" "2.0.5"
semver "^7.5.4" semver "^7.5.4"
vue@^3.4.21: vue@^3.4.21: