diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f247b2d..2a397ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,9 +40,9 @@ jobs: with: python-version: "3.12" cache: "poetry" - - run: "SKIP_BUILD_FRONTEND=true poetry lock" - - run: "SKIP_BUILD_FRONTEND=true poetry install" - - run: "SKIP_BUILD_FRONTEND=true poetry build" + - run: "poetry lock" + - run: "poetry install" + - run: "poetry build" # Skips building frontend (not using task) build-logo: name: "Build logo" @@ -56,8 +56,8 @@ jobs: with: python-version: "3.12" cache: "poetry" - - run: "SKIP_BUILD_FRONTEND=true poetry lock" - - run: "SKIP_BUILD_FRONTEND=true poetry install" + - run: "poetry lock" + - run: "poetry install" - run: "poetry run python yacv_server/logo.py" - uses: "actions/upload-artifact@v4" with: @@ -77,8 +77,8 @@ jobs: with: python-version: "3.12" cache: "poetry" - - run: "SKIP_BUILD_FRONTEND=true poetry lock" - - run: "SKIP_BUILD_FRONTEND=true poetry install" + - run: "poetry lock" + - run: "poetry install" - run: "YACV_DISABLE_SERVER=true poetry run python example/object.py" - uses: "actions/upload-artifact@v4" with: diff --git a/.github/workflows/deploy2.yml b/.github/workflows/deploy2.yml index e638d84..554046e 100644 --- a/.github/workflows/deploy2.yml +++ b/.github/workflows/deploy2.yml @@ -35,11 +35,7 @@ jobs: - uses: "actions/download-artifact@v4" with: # Downloads all artifacts from the build job path: "./public" - - run: | # Merge the subdirectories of public into a single directory - for dir in public/*; do - mv "$dir/"* public/ - rmdir "$dir" - done + merge-multiple: true - uses: "actions/configure-pages@v5" - uses: "actions/upload-pages-artifact@v3" with: @@ -71,5 +67,6 @@ jobs: cache: "poetry" - run: "poetry install" - run: "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}" - - run: "poetry publish --build" + - run: "poetry run task build" # This task also builds the frontend (with reduced features for less size) + - run: "poetry publish" diff --git a/assets/licenses.txt b/assets/licenses.txt index f869012..bb39b4a 100644 --- a/assets/licenses.txt +++ b/assets/licenses.txt @@ -761,6 +761,36 @@ MIT License ----------- +The following npm package may be included in this product: + + - pako@2.1.0 + +This package contains the following license: + +(The MIT License) + +Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------- + The following npm package may be included in this product: - lie@3.3.0 @@ -1129,6 +1159,35 @@ BSD-3-Clause ----------- +The following npm package may be included in this product: + + - ws@8.18.3 + +This package contains the following license: + +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------- + The following npm package may be included in this product: - color-string@1.9.1 @@ -1301,6 +1360,42 @@ THE SOFTWARE. ----------- +The following npm package may be included in this product: + + - js-base64@3.7.7 + +This package contains the following license: + +Copyright (c) 2014, Dan Kogai +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of {{{project}}} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + The following npm package may be included in this product: - estree-walker@2.0.2 @@ -1537,6 +1632,126 @@ SOFTWARE. ----------- +The following npm package may be included in this product: + + - state-local@1.0.7 + +This package contains the following license: + +MIT License + +Copyright (c) 2020 Suren Atoyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + +The following npm package may be included in this product: + + - vue-demi@0.14.10 + +This package contains the following license: + +MIT License + +Copyright (c) 2020-present, Anthony Fu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + +The following npm package may be included in this product: + + - @monaco-editor/loader@1.5.0 + +This package contains the following license: + +MIT License + +Copyright (c) 2021 Suren Atoyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + +The following npm package may be included in this product: + + - @guolao/vue-monaco-editor@1.5.5 + +This package contains the following license: + +MIT License + +Copyright (c) 2022 guolao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + The following npm package may be included in this product: - @monogrid/gainmap-js@3.1.0 @@ -1597,6 +1812,16 @@ SOFTWARE. ----------- +The following npm package may be included in this product: + + - pyodide@0.28.0 + +This package contains the following license: + +MPL-2.0 + +----------- + The following npm packages may be included in this product: - @mdi/js@7.4.47 @@ -1771,6 +1996,36 @@ THE SOFTWARE. ----------- +The following npm package may be included in this product: + + - monaco-editor@0.52.2 + +This package contains the following license: + +The MIT License (MIT) + +Copyright (c) 2016 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + The following npm package may be included in this product: - vuetify@3.9.0 diff --git a/build.py b/build.py deleted file mode 100644 index 5e145c7..0000000 --- a/build.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -import subprocess - -if __name__ == "__main__": - # Building the frontend is optional - if os.getenv('SKIP_BUILD_FRONTEND') is None and os.path.exists('package.json'): - # When building the backend, make sure the frontend is built first - subprocess.run(['yarn', 'install'], check=True) - subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], check=True) diff --git a/frontend/App.vue b/frontend/App.vue index d757e7a..d7bab44 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -11,7 +11,8 @@ import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./mis import {SceneMgr} from "./misc/scene"; import {Document} from "@gltf-transform/core"; import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue"; -import {mdiPlus} from '@mdi/js' +import {mdiCube, mdiPlus, mdiScriptTextPlay} from '@mdi/js' +// @ts-expect-error import SvgIcon from '@jamescoyle/vue-icon'; // NOTE: The ModelViewer library is big (THREE.js), so we split it and import it asynchronously @@ -21,7 +22,7 @@ const ModelViewerWrapper = defineAsyncComponent({ delay: 0, }); -let openSidebarsByDefault: Ref = ref(window.innerWidth > 1200); +let openSidebarsByDefault: Ref = ref(window.innerWidth > window.innerHeight); const sceneUrl = ref("") const viewer: Ref | null> = ref(null); @@ -80,17 +81,27 @@ let networkMgr = new NetworkManager(); networkMgr.addEventListener('update-early', (e) => viewer.value?.onProgress((e as CustomEvent>).detail.length * 0.01)); networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent)); +let preloadingModels = ref>([]); (async () => { // Start loading all configured models ASAP let sett = await settings(); - watch(viewer, (newViewer) => { - if (newViewer) { - newViewer.setPosterText('Trying to load' + - ' models from:' + sett.preload.map((url: string) => '- ' + url + '').join("")); + if (sett.preload.length > 0) { + watch(viewer, (newViewer) => { + if (newViewer) { + newViewer.setPosterText('Trying to load' + + ' models from:' + sett.preload.map((url: string) => '- ' + url + '').join("")); + } + }); + for (let model of sett.preload) { + preloadingModels.value.push(model); + let removeFromPreloadingModels = () => { + preloadingModels.value = preloadingModels.value.filter((m) => m !== model); + }; + networkMgr.load(model).then(removeFromPreloadingModels).catch((e) => { + removeFromPreloadingModels() + console.error("Error preloading model", model, e); + }); } - }); - for (let model of sett.preload) { - await networkMgr.load(model); - } + } // else No preloaded models (useful for playground mode) })(); async function loadModelManual() { @@ -104,7 +115,28 @@ async function loadModelManual() { - + + +
+ No model loaded + +   Open playground... + + +   Load demo model... + + +   Load model manually... + + + Still trying to load the following: + + {{ model }}, + + + +
@@ -117,7 +149,7 @@ async function loadModelManual() { - + @@ -125,7 +157,7 @@ async function loadModelManual() { - + @@ -135,6 +167,6 @@ async function loadModelManual() { \ No newline at end of file diff --git a/frontend/misc/IfNotSmallBuild.vue b/frontend/misc/IfNotSmallBuild.vue new file mode 100644 index 0000000..ce13a66 --- /dev/null +++ b/frontend/misc/IfNotSmallBuild.vue @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/frontend/misc/Sidebar.vue b/frontend/misc/Sidebar.vue index 9785f25..00dc312 100644 --- a/frontend/misc/Sidebar.vue +++ b/frontend/misc/Sidebar.vue @@ -2,6 +2,7 @@ import {ref} from "vue"; import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs"; import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js' +// @ts-expect-error import SvgIcon from '@jamescoyle/vue-icon'; const props = defineProps<{ diff --git a/frontend/misc/gltf.ts b/frontend/misc/gltf.ts index 28dcf07..15f8081 100644 --- a/frontend/misc/gltf.ts +++ b/frontend/misc/gltf.ts @@ -1,10 +1,13 @@ import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core"; -import {unpartition, mergeDocuments} from "@gltf-transform/functions"; +import {mergeDocuments, unpartition} from "@gltf-transform/functions"; let io = new WebIO(); export let extrasNameKey = "__yacv_name"; export let extrasNameValueHelpers = "__helpers"; +// @ts-expect-error +let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__; + /** * Loads a GLB model from a URL and adds it to the document or replaces it if the names match. * @@ -27,18 +30,20 @@ export async function mergePartial(url: string, name: string, document: Document try { // Try to load fast if no extensions are used newDoc = await io.readBinary(new Uint8Array(buffer)); } catch (e) { // Fallback to wait for download and register big extensions - if (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) { + if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) { if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true; // WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?) let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions") + // @ts-expect-error let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js"); + // @ts-expect-error let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js"); io.registerExtensions([KHRDracoMeshCompression]) .registerDependencies({ 'draco3d.decoder': await dracoDecoderWeb.default({}), 'draco3d.encoder': await dracoEncoderWeb.default({}) }); - } else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) { + } else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) { if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true; let {EXTTextureWebP} = await import("@gltf-transform/extensions") io.registerExtensions([EXTTextureWebP]); diff --git a/frontend/misc/helpers.ts b/frontend/misc/helpers.ts index 0a74df4..1f8f526 100644 --- a/frontend/misc/helpers.ts +++ b/frontend/misc/helpers.ts @@ -82,7 +82,7 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) { * The grid is built as a box of triangles (representing lines) looking to the inside of the box. * This ensures that only the back of the grid is always visible, regardless of the camera position. */ -export async function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) { +export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) { // Create transformed positions for the inner faces of the box let allPositions: number[] = []; let allIndices: number[] = []; diff --git a/frontend/misc/scene.ts b/frontend/misc/scene.ts index beb7f57..e6c188b 100644 --- a/frontend/misc/scene.ts +++ b/frontend/misc/scene.ts @@ -1,5 +1,5 @@ import {type Ref} from 'vue'; -import {Document} from '@gltf-transform/core'; +import {Buffer, Document, Scene} from '@gltf-transform/core'; import {extrasNameKey, extrasNameValueHelpers, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf"; import {newAxes, newGridBox} from "./helpers"; import {Vector3} from "three/src/math/Vector3.js" @@ -80,7 +80,19 @@ export class SceneMgr { private static async reloadHelpers(sceneUrl: Ref, document: Document, reloadScene: boolean): Promise { let bb = SceneMgr.getBoundingBox(document); - if (!bb) return document; + if (!bb) return document; // Empty document, no helpers to show + + // If only the helpers remain, go back to the empty scene + let noOtherModels = true; + for (let elem of document.getGraph().listEdges().map(e => e.getChild())) { + if (elem.getExtras() && !(elem instanceof Scene) && !(elem instanceof Buffer) && + elem.getExtras()[extrasNameKey] !== extrasNameValueHelpers) { + // There are other elements in the document, so we can show the helpers + noOtherModels = false; + break; + } + } + if (noOtherModels) return await removeModel(extrasNameValueHelpers, document); // Create the helper axes and grid box let helpersDoc = new Document(); diff --git a/frontend/misc/settings.ts b/frontend/misc/settings.ts index 1829bdd..0bf0327 100644 --- a/frontend/misc/settings.ts +++ b/frontend/misc/settings.ts @@ -1,4 +1,7 @@ // These are the default values for the settings, which are overridden below +import {ungzip} from "pako"; +import {b66Decode} from "../tools/b66.ts"; + let settingsCache: any = null; export async function settings() { @@ -38,18 +41,50 @@ export async function settings() { "12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEW6urpaLVq8AAAACklEQVQI" + "12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="), + + // Playground settings + pg_code: "", // Automatically loaded and executed code for the playground + pg_code_url: "", // URL to load the code from (overrides pg_code) + pg_opacity_loading: -1, // Opacity of the code during first load and run (< 0 is 0.0 if preload and 0.9 if not) + pg_opacity_loaded: 0.9, // Opacity of the code after it has been run for the first time }; - // Auto-override any settings from the URL + // Auto-override any settings from the URL (either GET parameters or hash) const url = new URL(window.location.href); url.searchParams.forEach((value, key) => { if (key in settings) (settings as any)[key] = parseSetting(key, value, settings); }) + if (url.hash.length > 0) { // Hash has bigger limits as it is not sent to the server + const hash = url.hash.slice(1); + const hashParams = new URLSearchParams(hash); + hashParams.forEach((value, key) => { + if (key in settings) (settings as any)[key] = parseSetting(key, value, settings); + }); + } + + // Grab the code from the URL if it is set + if (settings.pg_code_url.length > 0) { + // If the code URL is set, override the code + try { + const response = await fetch(settings.pg_code_url); + if (response.ok) { + settings.pg_code = await response.text(); + } else { + console.warn("Failed to load code from URL:", settings.pg_code_url); + } + } catch (error) { + console.error("Error fetching code from URL:", settings.pg_code_url, error); + } + } // Get the default preload URL if not overridden (requires a fetch that is avoided if possible) for (let i = 0; i < settings.preload.length; i++) { let url = settings.preload[i]; if (url === '') { + if (settings.pg_code != "") { // means no preload URL if code is set + settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1)); + continue; // Skip this preload URL + } const possibleBackend = new URL("./?api_updates=true", window.location.href) await fetch(possibleBackend, {method: "HEAD"}).then((response) => { if (response.ok && response.headers.get("Content-Type") === "text/event-stream") { @@ -63,6 +98,20 @@ export async function settings() { } settings.preload[i] = url; } + + // Auto-decompress the code and other playground settings + if (settings.pg_code.length > 0) { + try { + settings.pg_code = ungzip(b66Decode(settings.pg_code), {to: 'string'}); + } catch (error) { + console.warn("Failed to decompress code (assuming raw code):", error); + } + if (settings.pg_opacity_loading < 0) { + // If the opacity is not set, use 0.0 if preload is set, otherwise 0.9 + settings.pg_opacity_loading = settings.preload.length > 0 ? 0.0 : 0.9; + } + } + settingsCache = settings; return settings; } diff --git a/frontend/models/Model.vue b/frontend/models/Model.vue index cef743f..47c2dd8 100644 --- a/frontend/models/Model.vue +++ b/frontend/models/Model.vue @@ -25,6 +25,7 @@ import { mdiVectorLine, mdiVectorRectangle } from '@mdi/js' +// @ts-expect-error import SvgIcon from '@jamescoyle/vue-icon'; import {BackSide, FrontSide} from "three/src/constants.js"; import {Box3} from "three/src/math/Box3.js"; @@ -34,6 +35,8 @@ import {Vector3} from "three/src/math/Vector3.js"; import type {MObject3D} from "../tools/Selection.vue"; import {toLineSegments} from "../misc/lines.js"; import {settings} from "../misc/settings.js" +import {currentSceneRotation} from "../viewer/lighting.ts"; +import {Matrix4} from "three/src/math/Matrix4.js"; const props = defineProps<{ meshes: Array, @@ -115,7 +118,8 @@ function onWireframeChange(newWireframe: boolean) { if (!scene || !sceneModel) return; sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { - if (child.material && child.material.wireframe !== newWireframe) { + let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh' + if (child.material && child.material.wireframe !== newWireframe && childIsFace) { child.material.wireframe = newWireframe; child.material.needsUpdate = true; } @@ -153,10 +157,11 @@ function onClipPlanesChange() { let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x); let offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y); let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z); + let rotSceneMatrix = new Matrix4().makeRotationY(currentSceneRotation); let planes = [ - new Plane(new Vector3(-1, 0, 0), offsetX), - new Plane(new Vector3(0, -1, 0), offsetY), - new Plane(new Vector3(0, 0, 1), -offsetZ), + new Plane(new Vector3(-1, 0, 0), offsetX).applyMatrix4(rotSceneMatrix), + new Plane(new Vector3(0, -1, 0), offsetY).applyMatrix4(rotSceneMatrix), + new Plane(new Vector3(0, 0, 1), -offsetZ).applyMatrix4(rotSceneMatrix), ]; if (clipPlaneSwappedX.value) planes[0].negate(); if (clipPlaneSwappedY.value) planes[1].negate(); @@ -227,9 +232,10 @@ function onEdgeWidthChange(newEdgeWidth: number) { line.userData.niceLine = line2; // line.parent!.remove(line); // Keep it for better raycast and selection! line2.userData.noHit = true; + line2.visible = enabledFeatures.value.includes(1); edgeWidthChangeCleanup.push(() => { line2.parent!.remove(line2); - line.visible = true; + line.visible = enabledFeatures.value.includes(1); props.viewer!!.onElemReady((elem) => { elem.removeEventListener('resize', () => resizeListener(elem)); }); diff --git a/frontend/models/Models.vue b/frontend/models/Models.vue index ba160d5..e1e5bdc 100644 --- a/frontend/models/Models.vue +++ b/frontend/models/Models.vue @@ -7,7 +7,7 @@ import Model from "./Model.vue"; import {inject, ref, type Ref} from "vue"; const props = defineProps<{ viewer: InstanceType | null }>(); -const emit = defineEmits<{ remove: [string] }>() +const emit = defineEmits<{ removeModel: [string] }>() let {sceneDocument} = inject<{ sceneDocument: Ref }>('sceneDocument')!!; @@ -32,7 +32,7 @@ function meshName(mesh: Mesh) { } function onRemove(mesh: Mesh) { - emit('remove', meshName(mesh)) + emit('removeModel', meshName(mesh)) } function findModel(name: string) { diff --git a/frontend/shims.d.ts b/frontend/shims.d.ts deleted file mode 100644 index e657ea2..0000000 --- a/frontend/shims.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Avoids typescript error when importing some files -declare module '@jamescoyle/vue-icon' -declare module 'three-orientation-gizmo/src/OrientationGizmo' -declare module 'three/examples/jsm/libs/draco/draco_decoder.js' -declare module 'three/examples/jsm/libs/draco/draco_encoder.js' \ No newline at end of file diff --git a/frontend/tools/OrientationGizmo.vue b/frontend/tools/OrientationGizmo.vue index dcbad7d..532bd51 100644 --- a/frontend/tools/OrientationGizmo.vue +++ b/frontend/tools/OrientationGizmo.vue @@ -1,12 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/frontend/tools/Selection.vue b/frontend/tools/Selection.vue index 73f6386..3a22359 100644 --- a/frontend/tools/Selection.vue +++ b/frontend/tools/Selection.vue @@ -1,6 +1,7 @@ @@ -474,8 +479,8 @@ window.addEventListener('keydown', (event) => { variant="underlined"/> - - (H)ighlight the next clicked element in the models list + + (O)pen the next clicked element in the models list diff --git a/frontend/tools/Tools.vue b/frontend/tools/Tools.vue index 40c49a6..d56798e 100644 --- a/frontend/tools/Tools.vue +++ b/frontend/tools/Tools.vue @@ -13,13 +13,26 @@ import { import OrientationGizmo from "./OrientationGizmo.vue"; import type {PerspectiveCamera} from "three/src/cameras/PerspectiveCamera.js"; import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js"; -import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js' +import { + mdiClose, + mdiCrosshairsGps, + mdiDownload, + mdiGithub, + mdiLicense, + mdiLightbulb, + mdiProjector, + mdiScriptTextPlay +} from '@mdi/js' +// @ts-expect-error import SvgIcon from '@jamescoyle/vue-icon'; import type {ModelViewerElement} from '@google/model-viewer'; import Loading from "../misc/Loading.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import {defineAsyncComponent, ref, type Ref} from "vue"; import type {SelectionInfo} from "./selection"; +import {settings} from "../misc/settings.ts"; +import type {NetworkUpdateEvent} from "../misc/network.ts"; +import IfNotSmallBuild from "../misc/IfNotSmallBuild.vue"; const SelectionComponent = defineAsyncComponent({ loader: () => import("./Selection.vue"), @@ -34,9 +47,22 @@ const LicensesDialogContent = defineAsyncComponent({ delay: 0, }); +const PlaygroundDialogContent = defineAsyncComponent({ + loader: () => import("./PlaygroundDialogContent.vue"), + loadingComponent: Loading, + delay: 0, +}); + let props = defineProps<{ viewer: InstanceType | null }>(); -const emit = defineEmits<{ findModel: [string] }>() +const emit = defineEmits<{ findModel: [string], updateModel: [NetworkUpdateEvent] }>() + +const sett = ref(null); +const showPlaygroundDialog = ref(false); +(async () => { + sett.value = await settings(); + showPlaygroundDialog.value = sett.value.pg_code != ""; +})(); let selection: Ref> = ref([]); let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length @@ -114,10 +140,14 @@ function removeObjectSelections(objName: string) { selectionComp.value?.updateDistances(); } -defineExpose({removeObjectSelections}); +defineExpose({removeObjectSelections, openPlayground: () => showPlaygroundDialog.value = true}); // Add keyboard shortcuts -window.addEventListener('keydown', (event) => { +document.addEventListener('keydown', (event) => { + if ((event.target as any)?.tagName && ((event.target as any).tagName === 'INPUT' || (event.target as any).tagName === 'TEXTAREA')) { + // Ignore key events when an input is focused, except for text inputs + return; + } if (event.key === 'p') toggleProjection(); else if (event.key === 'c') centerCamera(); else if (event.key === 'd') downloadSceneGlb(); @@ -139,6 +169,13 @@ window.addEventListener('keydown', (event) => { Re(c)enter Camera + + To rotate the light hold shift and drag the mouse or use two fingers
+ Note that this breaks slightly clipping planes for now... (restart to fix)
+ + + +
Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)
{
Extras
+ + + + (D)ownload Scene - +