refactor most of the frontend and add permissive cors to backend

This commit is contained in:
Yeicor
2024-02-24 17:22:58 +01:00
parent 7dfdbdd34c
commit a72cc8dd09
13 changed files with 149 additions and 148 deletions

16
poetry.lock generated
View File

@@ -96,6 +96,20 @@ yarl = ">=1.0,<2.0"
[package.extras] [package.extras]
speedups = ["Brotli", "aiodns", "brotlicffi"] speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiohttp-cors"
version = "0.7.0"
description = "CORS support for aiohttp"
optional = false
python-versions = "*"
files = [
{file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"},
{file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"},
]
[package.dependencies]
aiohttp = ">=1.1"
[[package]] [[package]]
name = "aiohttp-devtools" name = "aiohttp-devtools"
version = "1.1.2" version = "1.1.2"
@@ -1597,4 +1611,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "950e4ebfaa15d8cc389403d0f931a63b2c2685aa1afbf3624e5806152acd6c83" content-hash = "92566c9fc24a286eea553f28b583fe6de53d7ecb595be508a0ce4ae9ca9f58c6"

View File

@@ -15,6 +15,7 @@ ocp-tessellate = "^2.0.6"
# Web # Web
aiohttp = "^3.9.3" aiohttp = "^3.9.3"
aiohttp-cors = "^0.7.0"
aiohttp-devtools = "^1.1.2" aiohttp-devtools = "^1.1.2"
# Misc # Misc

View File

@@ -3,13 +3,14 @@
import {defineAsyncComponent, ref, Ref} from "vue"; import {defineAsyncComponent, ref, Ref} from "vue";
import Sidebar from "./misc/Sidebar.vue"; import Sidebar from "./misc/Sidebar.vue";
import Loading from "./misc/Loading.vue"; import Loading from "./misc/Loading.vue";
import ModelViewerOverlay from "./viewer/ModelViewerOverlay.vue";
import Tools from "./tools/Tools.vue"; import Tools from "./tools/Tools.vue";
import Models from "./models/Models.vue"; import Models from "./models/Models.vue";
import {VLayout, VMain, VToolbarTitle, VTooltip, VBtn} from "vuetify/lib/components"; import {VLayout, VMain, VToolbarTitle} from "vuetify/lib/components";
import {settings} from "./misc/settings"; import {settings} from "./misc/settings";
import {NetworkManager, NetworkUpdateEvent} from "./misc/network"; import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
import {SceneMgr} from "./misc/scene"; import {SceneMgr} from "./misc/scene";
import {Document} from "@gltf-transform/core";
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
// NOTE: The ModelViewer library is big (THREE.js), so we split it and import it asynchronously // NOTE: The ModelViewer library is big (THREE.js), so we split it and import it asynchronously
const ModelViewerWrapper = defineAsyncComponent({ const ModelViewerWrapper = defineAsyncComponent({
@@ -20,12 +21,14 @@ const ModelViewerWrapper = defineAsyncComponent({
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200); let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
let [refSData, sData] = SceneMgr.newData(); let sceneUrl = ref("")
let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
let document = new Document();
// Set up the load model event listener // Set up the load model event listener
let networkMgr = new NetworkManager(); let networkMgr = new NetworkManager();
networkMgr.addEventListener('update', (model: NetworkUpdateEvent) => { networkMgr.addEventListener('update', async (model: NetworkUpdateEvent) => {
SceneMgr.loadModel(refSData, sData, model.name, model.url); document = await SceneMgr.loadModel(sceneUrl, document, model.name, model.url);
}); });
// Start loading all configured models ASAP // Start loading all configured models ASAP
for (let model of settings.preloadModels) { for (let model of settings.preloadModels) {
@@ -38,8 +41,7 @@ for (let model of settings.preloadModels) {
<!-- The main content of the app is the model-viewer with the SVG "2D" overlay --> <!-- The main content of the app is the model-viewer with the SVG "2D" overlay -->
<v-main id="main"> <v-main id="main">
<model-viewer-wrapper :src="refSData.viewerSrc" @load="(i) => SceneMgr.onload(refSData, i)"/> <model-viewer-wrapper ref="viewer" :src="sceneUrl"/>
<model-viewer-overlay v-if="refSData.viewer !== null"/>
</v-main> </v-main>
<!-- The left collapsible sidebar has the list of models --> <!-- The left collapsible sidebar has the list of models -->
@@ -47,15 +49,15 @@ for (let model of settings.preloadModels) {
<template #toolbar> <template #toolbar>
<v-toolbar-title>Models</v-toolbar-title> <v-toolbar-title>Models</v-toolbar-title>
</template> </template>
<models/> <models :viewer="viewer"/>
</sidebar> </sidebar>
<!-- The right collapsible sidebar has the list of tools --> <!-- The right collapsible sidebar has the list of tools -->
<sidebar :opened-init="openSidebarsByDefault" side="right" :width="48 * 3 /* buttons */ + 1"> <sidebar :opened-init="openSidebarsByDefault" side="right" :width="48 * 3 /* buttons */ + 1 /* border? */">
<template #toolbar> <template #toolbar>
<v-toolbar-title>Tools</v-toolbar-title> <v-toolbar-title>Tools</v-toolbar-title>
</template> </template>
<tools :ref-s-data="refSData"/> <tools :viewer="viewer"/>
</sidebar> </sidebar>
</v-layout> </v-layout>

View File

@@ -4,15 +4,15 @@ import {unpartition} from "@gltf-transform/functions";
let io = new WebIO(); let io = new WebIO();
/** /**
* Given the bytes of a GLB file and a parsed GLTF document, it parses and merges the GLB into the document. * Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
* *
* It can replace previous models in the document if the provided name matches the name of a previous model. * It can replace previous models in the document if the provided name matches the name of a previous model.
* *
* Remember to call mergeFinalize after all models have been merged (slower required operations). * Remember to call mergeFinalize after all models have been merged (slower required operations).
*/ */
export async function mergePartial(glb: Uint8Array, name: string, document: Document): Promise<Document> { export async function mergePartial(url: string, name: string, document: Document): Promise<Document> {
// Load the new document // Load the new document
let newDoc = await io.readBinary(glb); let newDoc = await io.read(url);
// Remove any previous model with the same name and ensure consistent names // Remove any previous model with the same name and ensure consistent names
// noinspection TypeScriptValidateJSTypes // noinspection TypeScriptValidateJSTypes

View File

@@ -16,7 +16,7 @@ export class NetworkManager extends EventTarget {
private knownObjectHashes: { [name: string]: string } = {}; private knownObjectHashes: { [name: string]: string } = {};
/** /**
* Tries to load a new model (.glb or .glbs) from the given URL. * Tries to load a new model (.glb) from the given URL.
* *
* If the URL uses the websocket protocol (ws:// or wss://), the server will be continuously monitored for changes. * If the URL uses the websocket protocol (ws:// or wss://), the server will be continuously monitored for changes.
* In this case, it will only trigger updates if the name or hash of any model changes. * In this case, it will only trigger updates if the name or hash of any model changes.
@@ -56,7 +56,7 @@ export class NetworkManager extends EventTarget {
private foundModel(name: string, hash: string, url: string) { private foundModel(name: string, hash: string, url: string) {
let prevHash = this.knownObjectHashes[name]; let prevHash = this.knownObjectHashes[name];
if (hash !== prevHash) { if (!hash || hash !== prevHash) {
this.knownObjectHashes[name] = hash; this.knownObjectHashes[name] = hash;
this.dispatchEvent(new NetworkUpdateEvent(name, url)); this.dispatchEvent(new NetworkUpdateEvent(name, url));
} }

View File

@@ -1,76 +1,37 @@
import type {ModelViewerElement} from '@google/model-viewer'; import type {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 {Ref, ref} from 'vue'; import {Ref} from 'vue';
import {Document} from '@gltf-transform/core'; import {Document} from '@gltf-transform/core';
import {ModelViewerInfo} from "./viewer/ModelViewerWrapper.vue"; import {mergeFinalize, mergePartial, toBuffer} from "./gltf";
import {mergeFinalize, mergePartial, toBuffer} from "../models/glb/merge";
export type SceneMgrRefData = {
/** When updated, forces the viewer to load a new model replacing the current one */
viewerSrc: string | null
/** The model viewer HTML element with all APIs */
viewer: ModelViewerElement | null
/** The (hidden) scene of the model viewer */
viewerScene: ModelScene | null
}
export type SceneMgrData = {
/** The currently shown document, which must match the viewerSrc. */
document: Document
}
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */ /** This class helps manage SceneManagerData. All methods are static to support reactivity... */
export class SceneMgr { export class SceneMgr {
private constructor() { /** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
} static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string): Promise<Document> {
/** Creates a new SceneManagerData object */
static newData(): [Ref<SceneMgrRefData>, SceneMgrData] {
let refData: any = ref({
viewerSrc: null,
viewer: null,
viewerScene: null,
});
let data = {
document: new Document()
};
// newVar.value.document.createScene("scene");
// this.showCurrentDoc(newVar.value).then(r => console.log("Initial empty model loaded"));
return [refData, data];
}
/** Loads a GLB/GLBS model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(refData: Ref<SceneMgrRefData>, data: SceneMgrData, name: string, url: string) {
let loadStart = performance.now(); let loadStart = performance.now();
// Connect to the URL of the model
let response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch model: " + response.statusText);
// Start merging into the current document, replacing or adding as needed // Start merging into the current document, replacing or adding as needed
let glb = new Uint8Array(await response.arrayBuffer()); document = await mergePartial(url, name, document);
data.document = await mergePartial(glb, name, data.document);
// Display the final fully loaded model // Display the final fully loaded model
await this.showCurrentDoc(refData, data); document = await this.showCurrentDoc(sceneUrl, document);
console.log("Model", name, "loaded in", performance.now() - loadStart, "ms"); console.log("Model", name, "loaded in", performance.now() - loadStart, "ms");
return document;
} }
/** Serializes the current document into a GLB and updates the viewerSrc */ /** Serializes the current document into a GLB and updates the viewerSrc */
private static async showCurrentDoc(refData: Ref<SceneMgrRefData>, data: SceneMgrData) { private static async showCurrentDoc(sceneUrl: Ref<string>, document: Document): Promise<Document> {
data.document = await mergeFinalize(data.document); // Make sure the document is fully loaded and ready to be shown
let buffer = await toBuffer(data.document); document = await mergeFinalize(document);
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
console.log("Showing current doc", data.document, "as", Array.from(buffer));
refData.value.viewerSrc = URL.createObjectURL(blob);
}
/** Should be called any model finishes loading successfully (after a viewerSrc update) */ // Serialize the document into a GLB and update the viewerSrc
static onload(data: SceneMgrRefData, info: typeof ModelViewerInfo) { let buffer = await toBuffer(document);
console.log("ModelViewer loaded", info); let blob = new Blob([buffer], {type: 'model/gltf-binary'});
data.viewer = info.viewer; //console.log("Showing current doc", document, "as", Array.from(buffer));
data.viewerScene = info.scene; sceneUrl.value = URL.createObjectURL(blob);
// Return the updated document
return document;
} }
} }

View File

@@ -6,7 +6,7 @@ export const settings = {
// @ts-ignore // @ts-ignore
new URL('../../assets/logo.glb', import.meta.url).href, new URL('../../assets/logo.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://localhost:8080/" // "ws://localhost:32323/"
], ],
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
checkServerEveryMs: 100, /* How often to check for a new server */ checkServerEveryMs: 100, /* How often to check for a new server */

View File

@@ -1,9 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {VExpansionPanel, VExpansionPanels, VExpansionPanelText, VExpansionPanelTitle} from "vuetify/lib/components"; import {VExpansionPanel, VExpansionPanels, VExpansionPanelText, VExpansionPanelTitle} from "vuetify/lib/components";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import Loading from "../misc/Loading.vue";
let props = defineProps<{ viewer: typeof ModelViewerWrapper | null }>();
</script> </script>
<template> <template>
<v-expansion-panels> <Loading v-if="!props.viewer"/>
<v-expansion-panels v-else>
<v-expansion-panel key="model-id"> <v-expansion-panel key="model-id">
<v-expansion-panel-title>? F ? E ? V | Model Name</v-expansion-panel-title> <v-expansion-panel-title>? F ? E ? V | Model Name</v-expansion-panel-title>
<v-expansion-panel-text>Content</v-expansion-panel-text> <v-expansion-panel-text>Content</v-expansion-panel-text>

View File

@@ -7,19 +7,20 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc
import {mdiCursorDefaultClick} from '@mdi/js'; import {mdiCursorDefaultClick} from '@mdi/js';
import type {Intersection, Material, Object3D} from "three"; import type {Intersection, Material, Object3D} from "three";
import {Raycaster} from "three"; import {Raycaster} from "three";
import type ModelViewerWrapperT from "./ModelViewerWrapper.vue";
export type MObject3D = Object3D & { export type MObject3D = Object3D & {
userData: { noHit?: boolean }, userData: { noHit?: boolean },
material: Material & { color: { r: number, g: number, b: number }, __prevBaseColorFactor?: [number, number, number] } material: Material & { color: { r: number, g: number, b: number }, __prevBaseColorFactor?: [number, number, number] }
}; };
let props = defineProps<{ viewer: ModelViewerElement, scene: ModelScene }>(); let props = defineProps<{ viewer: typeof ModelViewerWrapperT | null }>();
let selectionEnabled = ref(false); let selectionEnabled = ref(false);
let selectedMaterials = defineModel<Array<Intersection<MObject3D>>>({default: []}); let selectedMaterials = defineModel<Array<Intersection<MObject3D>>>({default: []});
let hasListener = false; let hasListener = false;
let mouseDownAt: [number, number] | null = null; let mouseDownAt: [number, number] | null = null;
let selectFilter = ref('Faces'); let selectFilter = ref('Faces');
const raycaster = new Raycaster(); const ray_caster = new Raycaster();
let selectionMoveListener = (event: MouseEvent) => { let selectionMoveListener = (event: MouseEvent) => {
if (!selectionEnabled.value) return; if (!selectionEnabled.value) return;
@@ -39,19 +40,19 @@ let selectionListener = (event: MouseEvent) => {
} }
mouseDownAt = undefined; mouseDownAt = undefined;
} }
let scene: ModelScene = props.scene; let scene: ModelScene = props.viewer?.scene;
// NOTE: Need to access internal as the API has issues with small faces surrounded by edges // NOTE: Need to access internal as the API has issues with small faces surrounded by edges
const ndcCoords = scene.getNDC(event.clientX, event.clientY); const ndcCoords = scene.getNDC(event.clientX, event.clientY);
//const hit = scene.hitFromPoint(ndcCoords) as Intersection<MObject3D> | undefined; //const hit = scene.hitFromPoint(ndcCoords) as Intersection<MObject3D> | undefined;
raycaster.setFromCamera(ndcCoords, (scene as any).camera); ray_caster.setFromCamera(ndcCoords, (scene as any).camera);
if ((scene as any).camera.isOrthographicCamera) { if ((scene as any).camera.isOrthographicCamera) {
// Need to fix the ray direction for ortho camera // Need to fix the ray direction for ortho camera
// FIXME: Still buggy (but less so :) // FIXME: Still buggy (but less so :)
raycaster.ray.direction.copy( ray_caster.ray.direction.copy(
scene.getTarget().clone().add(scene.target.position).sub((scene as any).camera.position).normalize()); scene.getTarget().clone().add(scene.target.position).sub((scene as any).camera.position).normalize());
} }
console.log('NDC', ndcCoords, 'Camera', (scene as any).camera, 'Ray', raycaster.ray); console.log('NDC', ndcCoords, 'Camera', (scene as any).camera, 'Ray', ray_caster.ray);
const hits = raycaster.intersectObject(scene, true); const hits = ray_caster.intersectObject(scene, true);
console.log(hits) console.log(hits)
let hit = hits.find((hit) => { let hit = hits.find((hit) => {
let isFace = hit.faceIndex !== null; let isFace = hit.faceIndex !== null;
@@ -68,6 +69,7 @@ let selectionListener = (event: MouseEvent) => {
select(hit) select(hit)
} }
} }
scene.queueRender() // Force rerender of model-viewer
}; };
function select(hit: Intersection<MObject3D>) { function select(hit: Intersection<MObject3D>) {
@@ -107,7 +109,7 @@ function deselectAll(alsoRemove = true) {
} }
function toggleSelection() { function toggleSelection() {
let viewer: ModelViewerElement = props.viewer; let viewer: ModelViewerElement = props.viewer?.elem;
if (!viewer) return; if (!viewer) return;
selectionEnabled.value = !selectionEnabled.value; selectionEnabled.value = !selectionEnabled.value;
if (selectionEnabled.value) { if (selectionEnabled.value) {
@@ -122,6 +124,7 @@ function toggleSelection() {
} else { } else {
deselectAll(false); deselectAll(false);
} }
props.viewer.scene.queueRender() // Force rerender of model-viewer
} }
</script> </script>

View File

@@ -16,11 +16,11 @@ import type {PerspectiveCamera} from "three/src/cameras/PerspectiveCamera";
import {OrthographicCamera} from "three/src/cameras/OrthographicCamera"; import {OrthographicCamera} from "three/src/cameras/OrthographicCamera";
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js' import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue'; import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue';
import {SceneMgrRefData} from "../misc/scene";
import type {ModelViewerElement} from '@google/model-viewer'; import type {ModelViewerElement} from '@google/model-viewer';
import type {Intersection} from "three"; import type {Intersection} from "three";
import type {MObject3D} from "./Selection.vue"; import type {MObject3D} from "./Selection.vue";
import Loading from "../misc/Loading.vue"; import Loading from "../misc/Loading.vue";
import type ModelViewerWrapper from "./viewer/ModelViewerWrapper.vue";
const SelectionComponent = defineAsyncComponent({ const SelectionComponent = defineAsyncComponent({
loader: () => import("./Selection.vue"), loader: () => import("./Selection.vue"),
@@ -35,11 +35,11 @@ const LicensesDialogContent = defineAsyncComponent({
}); });
let props = defineProps<{ refSData: SceneMgrRefData }>(); let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
let selection: Ref<Array<Intersection<typeof MObject3D>>> = ref([]); let selection: Ref<Array<Intersection<typeof MObject3D>>> = ref([]);
function syncOrthoCamera(force: boolean) { function syncOrthoCamera(force: boolean) {
let scene = props.refSData.viewerScene; let scene = props.viewer?.scene;
if (!scene) return; if (!scene) return;
let perspectiveCam: PerspectiveCamera = (scene as any).__perspectiveCamera; let perspectiveCam: PerspectiveCamera = (scene as any).__perspectiveCamera;
if (force || perspectiveCam && scene.camera != perspectiveCam) { if (force || perspectiveCam && scene.camera != perspectiveCam) {
@@ -50,13 +50,14 @@ function syncOrthoCamera(force: boolean) {
(scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far); (scene as any).camera = new OrthographicCamera(-w, w, h, -h, perspectiveCam.near, perspectiveCam.far);
scene.camera.position.copy(perspectiveCam.position); scene.camera.position.copy(perspectiveCam.position);
scene.camera.lookAt(scene.getTarget().clone().add(scene.target.position)); scene.camera.lookAt(scene.getTarget().clone().add(scene.target.position));
if (force) props.viewer.scene.queueRender() // Force rerender of model-viewer
requestAnimationFrame(() => syncOrthoCamera(false)); requestAnimationFrame(() => syncOrthoCamera(false));
} }
} }
let toggleProjectionText = ref('PERSP'); // Default to perspective camera let toggleProjectionText = ref('PERSP'); // Default to perspective camera
function toggleProjection() { function toggleProjection() {
let scene = props.refSData.viewerScene; let scene = props.viewer?.scene;
if (!scene) return; if (!scene) return;
let prevCam = scene.camera; let prevCam = scene.camera;
let wasPerspectiveCamera = prevCam.isPerspectiveCamera; let wasPerspectiveCamera = prevCam.isPerspectiveCamera;
@@ -67,21 +68,23 @@ function toggleProjection() {
} else { } else {
// Restore the default perspective camera // Restore the default perspective camera
scene.camera = (scene as any).__perspectiveCamera; scene.camera = (scene as any).__perspectiveCamera;
props.viewer.scene.queueRender() // Force rerender of model-viewer
} }
toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP'; toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP';
} }
function centerCamera() { function centerCamera() {
let viewer: ModelViewerElement = props.refSData.viewer; console.log('Centering camera', props.viewer);
if (!viewer) return; let viewerEl: ModelViewerElement = props.viewer?.elem;
viewer.updateFraming(); if (!viewerEl) return;
viewerEl.updateFraming();
} }
async function downloadSceneGlb() { async function downloadSceneGlb() {
let viewer = props.refSData.viewer; let viewerEl: ModelViewerElement = props.viewer?.elem;
if (!viewer) return; if (!viewerEl) return;
const glTF = await viewer.exportScene(); const glTF = await viewerEl.exportScene();
const file = new File([glTF], "export.glb"); const file = new File([glTF], "export.glb");
const link = document.createElement("a"); const link = document.createElement("a");
link.download = file.name; link.download = file.name;
@@ -96,7 +99,7 @@ async function openGithub() {
</script> </script>
<template> <template>
<orientation-gizmo :scene="props.refSData.viewerScene" v-if="props.refSData.viewerScene !== null"/> <orientation-gizmo :scene="props.viewer.scene" v-if="props.viewer?.scene"/>
<v-divider/> <v-divider/>
<h5>Camera</h5> <h5>Camera</h5>
<v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span> <v-btn icon @click="toggleProjection"><span class="icon-detail">{{ toggleProjectionText }}</span>
@@ -111,7 +114,7 @@ async function openGithub() {
</v-btn> </v-btn>
<v-divider/> <v-divider/>
<h5>Selection ({{ selection.filter((s) => s.face).length }}F {{ selection.filter((s) => !s.face).length }}E ?V)</h5> <h5>Selection ({{ selection.filter((s) => s.face).length }}F {{ selection.filter((s) => !s.face).length }}E ?V)</h5>
<selection-component :viewer="props.refSData.viewer" :scene="props.refSData.viewerScene" v-model="selection"/> <selection-component :viewer="props.viewer" v-model="selection"/>
<v-divider/> <v-divider/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<h5>Extras</h5> <h5>Extras</h5>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
// TODO: transparent SVG overlay that redirects and grabs some events
// https://modelviewer.dev/examples/annotations/index.html
</script>
<template>
<div class="overlay-svg-wrapper">
<svg class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<!-- <rect x="0" y="0" width="100%" height="100%" fill="transparent"/>-->
</svg>
</div>
</template>
<style scoped>
.overlay-svg-wrapper {
position: relative;
top: -100%;
left: 0;
width: 100%;
height: 0;
pointer-events: none;
z-index: 1;
}
.overlay-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100dvh;
pointer-events: none;
}
</style>

View File

@@ -1,35 +1,41 @@
<script lang="ts">
</script>
<script setup lang="ts"> <script setup lang="ts">
import {settings} from "../misc/settings"; import {settings} from "../misc/settings";
import {ModelViewerElement} from '@google/model-viewer'; import {onMounted} from "vue";
import {onMounted, ref} from "vue";
import {$scene} from "@google/model-viewer/lib/model-viewer-base"; import {$scene} from "@google/model-viewer/lib/model-viewer-base";
import Loading from "../misc/Loading.vue";
import {ref} from "vue";
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";
export type ModelViewerInfo = { viewer: ModelViewerElement, scene: ModelScene };
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
const emit = defineEmits<{ const emit = defineEmits<{
load: [info: ModelViewerInfo] // noinspection ThisExpressionReferencesGlobalObjectJS
load: []
}>() }>()
const props = defineProps({ const props = defineProps({
src: String src: String
}); });
let viewer = ref<ModelViewerElement | null>(null); const elem = ref<ModelViewerElement | null>(null);
const scene = ref<ModelScene | null>(null);
defineExpose({elem, scene});
onMounted(() => { onMounted(() => {
viewer.value.addEventListener('load', () => { elem.value.addEventListener('load', () => {
if (viewer.value) { if (elem.value) {
// Delete the initial load banner // Delete the initial load banner
// TODO: Replace with an actual poster? // TODO: Replace with an actual poster?
let banner = viewer.value.querySelector('.initial-load-banner'); let banner = elem.value.querySelector('.initial-load-banner');
if (banner) banner.remove(); if (banner) banner.remove();
// Set the scene
scene.value = elem.value[$scene] as ModelScene;
// Emit the load event // Emit the load event
emit('load', { emit('load')
viewer: viewer.value,
scene: viewer.value[$scene] as ModelScene,
})
} }
}); });
}); });
@@ -37,15 +43,22 @@ onMounted(() => {
</script> </script>
<template> <template>
<model-viewer ref="viewer" <model-viewer ref="elem" style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls
style="width: 100%; height: 100%" :src="props.src" alt="The 3D model(s)" camera-controls camera-orbit="30deg 75deg auto" camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto"
max-camera-orbit="Infinity 180deg auto" min-camera-orbit="-Infinity 0deg 5%" disable-tap min-camera-orbit="-Infinity 0deg 5%" disable-tap :exposure="settings.exposure"
:exposure="settings.exposure" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
:autoplay="settings.autoplay" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
:skybox-image="settings.background" :environment-image="settings.background"> :environment-image="settings.background">
<slot></slot> <!-- Controls, annotations, etc. --> <slot></slot> <!-- Controls, annotations, etc. -->
<div class="annotation initial-load-banner">Loading models...</div> <loading class="annotation initial-load-banner"></loading>
</model-viewer> </model-viewer>
<!-- TODO: Transparent SVG overlay that can draw 2D lines attached to the 3D model(s) -->
<!-- https://modelviewer.dev/examples/annotations/index.html -->
<div class="overlay-svg-wrapper">
<svg class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<!--<line x1="0" y1="0" x2="100%" y2="100%" stroke="black" stroke-width="2"/>-->
</svg>
</div>
</template> </template>
<style scoped> <style scoped>
@@ -53,4 +66,24 @@ onMounted(() => {
:not(:defined) > * { :not(:defined) > * {
display: none; display: none;
} }
/* This is the SVG overlay that will be used for line annotations */
.overlay-svg-wrapper {
position: relative;
top: -100%;
left: 0;
width: 100%;
height: 0;
pointer-events: none;
z-index: 1;
}
.overlay-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100dvh;
pointer-events: none;
}
</style> </style>

View File

@@ -9,6 +9,7 @@ from dataclasses import dataclass, field
from threading import Thread from threading import Thread
from typing import Optional, Dict, Union from typing import Optional, Dict, Union
import aiohttp_cors
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from aiohttp import web from aiohttp import web
from build123d import Shape, Axis from build123d import Shape, Axis
@@ -59,6 +60,16 @@ class Server:
# - 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') self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend')
# --- CORS ---
cors = aiohttp_cors.setup(self.app, defaults={
"*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
allow_headers="*",
)
})
for route in list(self.app.router.routes()):
cors.add(route)
# --- Misc --- # --- Misc ---
self.loop = asyncio.new_event_loop() self.loop = asyncio.new_event_loop()
@@ -197,7 +208,7 @@ class Server:
"""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) # Export the object (or fail if not found)
exported_glb = self.export(request.match_info['name']) exported_glb = await self.export(request.match_info['name'])
response = web.Response() response = web.Response()
try: try:
# Create a new stream response with custom content type and headers # Create a new stream response with custom content type and headers