Compare commits

..

18 Commits

Author SHA1 Message Date
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
25 changed files with 356 additions and 187 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:

View File

@@ -21,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:
@@ -29,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

@@ -17,7 +17,9 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
deploy: # TODO: Update versions automatically
deploy-frontend:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
environment: environment:
name: "github-pages" name: "github-pages"
@@ -35,18 +37,24 @@ jobs:
name: "logo" name: "logo"
path: "./public" path: "./public"
allow_forks: false allow_forks: false
- uses: "svenstaro/upload-release-action@v2" - uses: "dawidd6/action-download-artifact@v3"
with: with:
repo_token: "${{ secrets.GITHUB_TOKEN }}" workflow: "build.yml"
file: "./public/*" name: "example"
asset_name: "frontend" path: "./public"
tag: "${{ github.ref }}" allow_forks: false
overwrite: true
file_glob: true
- uses: "actions/configure-pages@v4" - uses: "actions/configure-pages@v4"
- uses: "actions/upload-pages-artifact@v3" - uses: "actions/upload-pages-artifact@v3"
with: with:
path: 'public' path: 'public'
- id: "deployment" - id: "deployment"
uses: "actions/deploy-pages@v4" uses: "actions/deploy-pages@v4"
- run: 'zip -r frontend.zip public'
- uses: "svenstaro/upload-release-action@v2"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
file: "frontend.zip"
tag: "${{ github.ref }}"
overwrite: 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 `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
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 `preload=...` 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 `preload` 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/?preload=base.glb&preload=fox.glb&preload=img.jpg.glb&preload=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
@@ -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.7
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,7 @@ import os
import subprocess import subprocess
if __name__ == "__main__": if __name__ == "__main__":
# When building the backend, make sure the frontend is built first if os.getenv('SKIP_BUILD_FRONTEND') is None:
subprocess.run(['yarn', 'install'], check=True) # When building the backend, make sure the frontend is built first
subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], check=True) 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

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

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

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

@@ -1,6 +1,6 @@
{ {
"name": "yet-another-cad-viewer", "name": "yet-another-cad-viewer",
"version": "0.1.0", "version": "0.3.0",
"description": "", "description": "",
"license": "MIT", "license": "MIT",
"author": "Yeicor <4929005+Yeicor@users.noreply.github.com>", "author": "Yeicor <4929005+Yeicor@users.noreply.github.com>",
@@ -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

@@ -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.3.0" # 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

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

@@ -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')
@@ -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,13 +12,14 @@ 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
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'
@@ -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,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()
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
@@ -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,35 +282,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,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

@@ -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.mylogger import logger
from cad import CADLike from yacv_server.cad import CADLike
from gltf import GLTFMgr from yacv_server.gltf import GLTFMgr
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

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