diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eacfc71..a4db24d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,12 @@ updates: interval: "weekly" day: "saturday" time: "09:00" + - package-ecosystem: "pip" + directory: "/example" + schedule: + interval: "weekly" + day: "saturday" + time: "09:00" - package-ecosystem: "github-actions" directory: "/.github/workflows/" schedule: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f351ef6..b0f661c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: - uses: "actions/upload-artifact@v4" with: name: "frontend" - path: "./dist" + path: "dist" retention-days: 5 build-backend: @@ -29,9 +29,6 @@ jobs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v4" - - uses: "actions/setup-node@v4" - with: - cache: "yarn" - run: "pipx install poetry" - uses: "actions/setup-python@v5" with: @@ -45,9 +42,6 @@ jobs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v4" - - uses: "actions/setup-node@v4" - with: - cache: "yarn" - run: "pipx install poetry" - uses: "actions/setup-python@v5" with: @@ -59,5 +53,23 @@ jobs: - uses: "actions/upload-artifact@v4" with: 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" + - uses: "actions/setup-python@v5" + with: + python-version: "3.11" + cache: "pip" + - run: "pip install -r example/requirements.txt" + - run: "cd example && python object.py" + - run: "mv example/export/object.glb example/export/example.glb" + - uses: "actions/upload-artifact@v4" + with: + name: "example" + path: "example/export" retention-days: 5 \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9c272b6..3b545c6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,6 +17,8 @@ concurrency: cancel-in-progress: false jobs: + # TODO: Update versions automatically + deploy-frontend: runs-on: "ubuntu-latest" environment: @@ -35,6 +37,12 @@ jobs: name: "logo" path: "./public" allow_forks: false + - uses: "dawidd6/action-download-artifact@v3" + with: + workflow: "build.yml" + name: "example" + path: "./public" + allow_forks: false - uses: "actions/configure-pages@v4" - uses: "actions/upload-pages-artifact@v3" with: diff --git a/README.md b/README.md index d5662c6..d4dd270 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ in a web browser. - View and interact with topological entities: faces, edges, vertices and locations. - Control clipping planes and transparency of each model. - 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. -- [Live lazy updates](#live-updates) while editing the CAD model (using the `yacv-server` package). +- Fully-featured static deployment: just upload the viewer and models to your server. +- Hot reloading while editing the CAD model (using the `yacv-server` package). -## 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 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 open [the viewer](https://yeicor-3d.github.io/yet-another-cad-viewer/) with diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..c42a1aa --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,2 @@ +/venv/ +/export/ \ No newline at end of file diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..c0cec5a --- /dev/null +++ b/example/README.md @@ -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=` + +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 + diff --git a/example/object.py b/example/object.py new file mode 100644 index 0000000..361c7ef --- /dev/null +++ b/example/object.py @@ -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') diff --git a/example/requirements.txt b/example/requirements.txt new file mode 100644 index 0000000..91ac386 --- /dev/null +++ b/example/requirements.txt @@ -0,0 +1,3 @@ +build123d==0.4.0 +yacv-server==0.1.0 +asyncio==3.4.3 \ No newline at end of file diff --git a/frontend/misc/Loading.vue b/frontend/misc/Loading.vue index 0f3c2fa..5460495 100644 --- a/frontend/misc/Loading.vue +++ b/frontend/misc/Loading.vue @@ -6,7 +6,8 @@ import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components/ - + + diff --git a/frontend/misc/network.ts b/frontend/misc/network.ts index 584d020..e0ae241 100644 --- a/frontend/misc/network.ts +++ b/frontend/misc/network.ts @@ -24,8 +24,10 @@ export class NetworkManager extends EventTarget { * Updates will be emitted as "update" events, including the download URL and the model name. */ async load(url: string) { - if (url.startsWith("ws://") || url.startsWith("wss://")) { - this.monitorWebSocket(url); + if (url.startsWith("dev+")) { + let baseUrl = new URL(url.slice(4)); + baseUrl.searchParams.set("api_updates", "true"); + this.monitorDevServer(baseUrl); } else { // Get the last part of the URL as the "name" of the model let name = url.split("/").pop(); @@ -38,21 +40,20 @@ export class NetworkManager extends EventTarget { } } - private monitorWebSocket(url: string) { + private monitorDevServer(url: string) { // WARNING: This will spam the console logs with failed requests when the server is down - let ws = new WebSocket(url); - ws.onmessage = (event) => { + let eventSource = new EventSource(url); + eventSource.onmessage = (event) => { let data = JSON.parse(event.data); console.debug("WebSocket message", data); let urlObj = new URL(url); - urlObj.protocol = urlObj.protocol === "ws:" ? "http:" : "https:"; + urlObj.searchParams.delete("api_updates"); urlObj.searchParams.set("api_object", data.name); this.foundModel(data.name, data.hash, urlObj.toString()); }; - ws.onerror = () => ws.close(); - ws.onclose = () => setTimeout(() => this.monitorWebSocket(url), settings.monitorEveryMs); - let timeoutFaster = setTimeout(() => ws.close(), settings.monitorOpenTimeoutMs); - ws.onopen = () => clearTimeout(timeoutFaster); + eventSource.onerror = () => { // Retry after a very short delay + setTimeout(() => this.monitorDevServer(url), settings.monitorEveryMs); + } } private foundModel(name: string, hash: string | null, url: string) { diff --git a/frontend/misc/settings.ts b/frontend/misc/settings.ts index e8e012c..4d67543 100644 --- a/frontend/misc/settings.ts +++ b/frontend/misc/settings.ts @@ -10,11 +10,11 @@ export const settings = { // @ts-ignore // new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href, // Websocket URLs automatically listen for new models from the python backend - "ws://127.0.0.1:32323/" + "dev+http://127.0.0.1:32323/" ], displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ monitorEveryMs: 100, - monitorOpenTimeoutMs: 100, + monitorOpenTimeoutMs: 1000, // ModelViewer settings autoplay: true, arModes: 'webxr scene-viewer quick-look', diff --git a/frontend/viewer/ModelViewerWrapper.vue b/frontend/viewer/ModelViewerWrapper.vue index 96e1edc..e59164b 100644 --- a/frontend/viewer/ModelViewerWrapper.vue +++ b/frontend/viewer/ModelViewerWrapper.vue @@ -1,12 +1,9 @@ - -