diff --git a/.gitignore b/.gitignore index 10b49b1..b2604fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Useful .gitignore templates: https://github.com/github/gitignore /node_modules/ /dist/ +/yacv_server/frontend/ /.cache/ /.parcel-cache/ /.idea/ diff --git a/build.py b/build.py new file mode 100644 index 0000000..4586cfa --- /dev/null +++ b/build.py @@ -0,0 +1,7 @@ +import os +import subprocess + +if __name__ == "__main__": + # When building the backend, make sure the frontend is built first + subprocess.run(['yarn', 'install'], check=True) + subprocess.run(['yarn', 'build', '--dist-dir', 'yacv_server/frontend'], check=True) diff --git a/package.json b/package.json index e89f76d..2279c41 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Yeicor", "scripts": { "start": "parcel src/index.html", - "build": "yarn update-licenses && parcel build src/index.html --reporter @parcel/reporter-bundle-analyzer --detailed-report --public-url ./", + "build": "yarn update-licenses && parcel build src/index.html --reporter @parcel/reporter-bundle-analyzer --detailed-report --public-url ./ --no-scope-hoist", "update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite" }, "dependencies": { diff --git a/pyproject.toml b/pyproject.toml index 5a8ad1a..265d039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,14 @@ [tool.poetry] name = "yacv-server" -version = "0.1.0" +version = "0.1.0" # TODO: Update automatically by CI on release description = "Yet Another CAD Viewer (server)" authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"] license = "MIT" readme = "README.md" +include = [ + { path = 'yacv_server/frontend/*', format = 'wheel' }, + { path = 'yacv_server/frontend/*', format = 'sdist' }, +] [tool.poetry.dependencies] python = "^3.9" @@ -22,6 +26,10 @@ aiohttp-devtools = "^1.1.2" pygltflib = "^1.16.1" pillow = "^10.2.0" +[tool.poetry.build] +generate-setup-file = false +script = "build.py" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/src/index.ts b/src/index.ts index 2ad02cd..54728f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,13 +2,12 @@ globalThis.__VUE_OPTIONS_API__ = process.env.NODE_ENV === "development" globalThis.__VUE_PROD_DEVTOOLS__ = process.env.NODE_ENV === "development" globalThis.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = process.env.NODE_ENV === "development" -// import {createApp} from 'vue/dist/vue.esm-browser.prod.js' -// import {createApp} from 'vue/dist/vue.esm-browser.js' import {createApp} from 'vue' import App from './App.vue' import {createVuetify} from 'vuetify'; import * as directives from 'vuetify/lib/directives'; +import 'vuetify/dist/vuetify.css'; const vuetify = createVuetify({ directives, @@ -20,7 +19,3 @@ const vuetify = createVuetify({ const app = createApp(App) app.use(vuetify) app.mount('body') - -// Start non-blocking loading of Vuetify styles -// @ts-ignore -import('vuetify/dist/vuetify.css'); \ No newline at end of file diff --git a/src/misc/network.ts b/src/misc/network.ts index f0d7ce0..7fe2a6b 100644 --- a/src/misc/network.ts +++ b/src/misc/network.ts @@ -39,6 +39,7 @@ export class NetworkManager extends EventTarget { } private monitorWebSocket(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 data = JSON.parse(event.data); @@ -48,13 +49,10 @@ export class NetworkManager extends EventTarget { urlObj.searchParams.set("api_object", data.name); this.foundModel(data.name, data.hash, urlObj.toString()); }; - ws.onerror = (event) => { - console.error("WebSocket error", event); - } - ws.onclose = () => { - console.debug("WebSocket closed, reconnecting very soon"); - setTimeout(() => this.monitorWebSocket(url), settings.checkServerEveryMs); - } + ws.onerror = () => ws.close(); + ws.onclose = () => setTimeout(() => this.monitorWebSocket(url), settings.monitorEveryMs); + let timeoutFaster = setTimeout(() => ws.close(), settings.monitorOpenTimeoutMs); + ws.onopen = () => clearTimeout(timeoutFaster); } private foundModel(name: string, hash: string, url: string) { diff --git a/src/misc/scene.ts b/src/misc/scene.ts index aca86c9..b9f9a29 100644 --- a/src/misc/scene.ts +++ b/src/misc/scene.ts @@ -1,4 +1,4 @@ -import {Ref, ShallowRef} from 'vue'; +import {Ref} from 'vue'; import {Document} from '@gltf-transform/core'; import {extrasNameKey, extrasNameValueHelpers, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf"; import {newAxes, newGridBox} from "./helpers"; @@ -19,6 +19,7 @@ export class SceneMgr { if (name !== extrasNameValueHelpers) { // Reload the helpers to fit the new model + // TODO: Only reload the helpers after a few milliseconds of no more models being added/removed document = await this.reloadHelpers(sceneUrl, document); } else { // Display the final fully loaded model diff --git a/src/misc/settings.ts b/src/misc/settings.ts index 32a5790..9ffd27e 100644 --- a/src/misc/settings.ts +++ b/src/misc/settings.ts @@ -2,18 +2,19 @@ export const settings = { preloadModels: [ // @ts-ignore - new URL('../../assets/fox.glb', import.meta.url).href, + // new URL('../../assets/fox.glb', import.meta.url).href, // @ts-ignore - new URL('../../assets/logo_build/base.glb', import.meta.url).href, + // new URL('../../assets/logo_build/base.glb', import.meta.url).href, // @ts-ignore - new URL('../../assets/logo_build/location.glb', import.meta.url).href, + // new URL('../../assets/logo_build/location.glb', import.meta.url).href, // @ts-ignore - new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href, + // new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href, // Websocket URLs automatically listen for new models from the python backend - // "ws://192.168.1.132:32323/" + "ws://127.0.0.1:32323/" ], displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ - checkServerEveryMs: 100, /* How often to check for a new server */ + monitorEveryMs: 100, + monitorOpenTimeoutMs: 10000, // ModelViewer settings autoplay: true, arModes: 'webxr scene-viewer quick-look', diff --git a/src/viewer/ModelViewerWrapper.vue b/src/viewer/ModelViewerWrapper.vue index 526c0d9..4970c75 100644 --- a/src/viewer/ModelViewerWrapper.vue +++ b/src/viewer/ModelViewerWrapper.vue @@ -28,7 +28,7 @@ onMounted(() => { // Delete the initial load banner let banner = elem.value.querySelector('.initial-load-banner'); if (banner) banner.remove(); - // Set the scene + // Set the scene and renderer scene.value = elem.value[$scene] as ModelScene; renderer.value = elem.value[$renderer] as Renderer; // Emit the load event @@ -162,11 +162,11 @@ watch(disableTap, (value) => { {{ line.centerText }} diff --git a/yacv_server/__init__.py b/yacv_server/__init__.py index 9ce7175..c687eb7 100644 --- a/yacv_server/__init__.py +++ b/yacv_server/__init__.py @@ -26,11 +26,12 @@ show_all = server.show_cad_all def _get_app() -> web.Application: """Required by aiohttp-devtools""" logging.basicConfig(level=logging.DEBUG) - from logo import build_logo + from logo import build_logo, ASSETS_DIR logo, img_location, img_path = build_logo() - server.show_cad(logo, 'Logo') - server.show_cad(img_location, 'Location') + server.show_cad(logo, 'logo') + server.show_cad(img_location, 'location') server.show_image(img_path, img_location, 20) + server.show_gltf(open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read(), 'fox') return server.app diff --git a/yacv_server/server.py b/yacv_server/server.py index 23727fe..36c97fe 100644 --- a/yacv_server/server.py +++ b/yacv_server/server.py @@ -20,7 +20,16 @@ from mylogger import logger from pubsub import BufferedPubSub from tessellate import _hashcode, tessellate -FRONTEND_BASE_PATH = os.getenv('FRONTEND_BASE_PATH', '../dist') +# Find the frontend folder (optional, but recommended) +FILE_DIR = os.path.dirname(__file__) +FRONTEND_BASE_PATH = os.getenv('FRONTEND_BASE_PATH', os.path.join(FILE_DIR, 'frontend')) +if not os.path.exists(FRONTEND_BASE_PATH): + if os.path.exists(os.path.join(FILE_DIR, '..', 'dist')): # Fallback to dev build + FRONTEND_BASE_PATH = os.path.join(FILE_DIR, '..', 'dist') + else: + logger.warning('Frontend not found at %s', FRONTEND_BASE_PATH) + +# Define the API paths (also available at the root path for simplicity) UPDATES_API_PATH = '/api/updates' OBJECTS_API_PATH = '/api/object' # /{name}