mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
cleaning up
This commit is contained in:
56
src/App.vue
56
src/App.vue
@@ -1,58 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import {defineAsyncComponent, ref, Ref} from "vue";
|
||||
import Sidebar from "./Sidebar.vue";
|
||||
import Loading from "./Loading.vue";
|
||||
import ModelViewerOverlay from "./ModelViewerOverlay.vue";
|
||||
import Tools from "./Tools.vue";
|
||||
import {
|
||||
VExpansionPanel,
|
||||
VExpansionPanels,
|
||||
VExpansionPanelText,
|
||||
VExpansionPanelTitle,
|
||||
VLayout,
|
||||
VMain,
|
||||
VToolbarTitle
|
||||
} from "vuetify/lib/components";
|
||||
import type {ModelViewerInfo} from "./ModelViewerWrapper.vue";
|
||||
import Sidebar from "./models/Sidebar.vue";
|
||||
import Loading from "./viewer/Loading.vue";
|
||||
import ModelViewerOverlay from "./viewer/ModelViewerOverlay.vue";
|
||||
import Tools from "./tools/Tools.vue";
|
||||
import Models from "./models/Models.vue";
|
||||
import {VLayout, VMain, VToolbarTitle} from "vuetify/lib/components";
|
||||
import type {ModelViewerInfo} from "./viewer/ModelViewerWrapper.vue";
|
||||
import {settings} from "./tools/settings";
|
||||
|
||||
// NOTE: The ModelViewer library is big, 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({
|
||||
loader: () => import('./ModelViewerWrapper.vue'),
|
||||
loader: () => import('./viewer/ModelViewerWrapper.vue'),
|
||||
loadingComponent: Loading,
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
// Open models by default on wide screens
|
||||
let openSidebarsDefault: Ref<boolean> = ref(window.innerWidth > 1200);
|
||||
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
|
||||
let modelViewerInfo: Ref<typeof ModelViewerInfo | null> = ref(null);
|
||||
let modelSrc: Ref<string | Uint8Array> = ref(settings.preloadModels[0]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-layout full-height>
|
||||
<!-- The main content of the app is the model-viewer with the SVG "2D" overlay -->
|
||||
<v-main id="main">
|
||||
<model-viewer-wrapper @load-viewer="(args) => {
|
||||
console.log('Model-Viewer finished loading:', args)
|
||||
modelViewerInfo = args
|
||||
}"/>
|
||||
<model-viewer-wrapper :src="modelSrc" @load-viewer="(args) => modelViewerInfo = args"/>
|
||||
<model-viewer-overlay v-if="modelViewerInfo"/>
|
||||
</v-main>
|
||||
<!-- The left collapsible sidebar has the list of models -->
|
||||
<sidebar :opened-init="openSidebarsDefault" side="left">
|
||||
<sidebar :opened-init="openSidebarsByDefault" side="left">
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Models</v-toolbar-title>
|
||||
</template>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel key="model-id">
|
||||
<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>
|
||||
</v-expansion-panels>
|
||||
<models :modelViewerInfo="modelViewerInfo"/>
|
||||
</sidebar>
|
||||
<!-- The right collapsible sidebar has the list of tools -->
|
||||
<sidebar :opened-init="openSidebarsDefault" side="right" :width="120">
|
||||
<sidebar :opened-init="openSidebarsByDefault" side="right" :width="120">
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Tools</v-toolbar-title>
|
||||
</template>
|
||||
@@ -68,11 +52,3 @@ html, body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<style scoped>
|
||||
/* Fix bug in hidden expansion panel text next to active expansion panel */
|
||||
.v-expansion-panel-title--active + .v-expansion-panel-text {
|
||||
display: flex !important;
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,6 @@ const vuetify = createVuetify({
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(vuetify)
|
||||
// noinspection JSUnresolvedReference
|
||||
app.mount('body')
|
||||
|
||||
// Start non-blocking loading of Vuetify and icon styles
|
||||
|
||||
24
src/models/Models.vue
Normal file
24
src/models/Models.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import {VExpansionPanel, VExpansionPanels, VExpansionPanelText, VExpansionPanelTitle} from "vuetify/lib/components";
|
||||
|
||||
const props = defineProps({
|
||||
modelViewerInfo: Object
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel key="model-id">
|
||||
<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>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<style scoped>
|
||||
/* Fix bug in hidden expansion panel text next to active expansion panel */
|
||||
.v-expansion-panel-title--active + .v-expansion-panel-text {
|
||||
display: flex !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
|
||||
import * as THREE from "three";
|
||||
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||
|
||||
window.THREE = THREE // HACK: Required for the gizmo to work
|
||||
|
||||
export class OrientationGizmo {
|
||||
element: OrientationGizmoRaw
|
||||
|
||||
constructor(scene: ModelScene) {
|
||||
// noinspection SpellCheckingInspection
|
||||
this.element = new OrientationGizmoRaw(scene.camera, {
|
||||
size: 120,
|
||||
bubbleSizePrimary: 12,
|
||||
bubbleSizeSeconday: 10,
|
||||
fontSize: "14px"
|
||||
});
|
||||
// Place in the top right corner
|
||||
this.element.style.position = "absolute";
|
||||
this.element.style.top = "0px";
|
||||
this.element.style.right = "0px";
|
||||
this.element.style.zIndex = "1000";
|
||||
// HACK: Swap axes to match A-Frame
|
||||
for (let swap of [["y", "-z"], ["z", "-y"], ["z", "-z"]]) {
|
||||
let indexA = this.element.bubbles.findIndex((bubble) => bubble.axis == swap[0])
|
||||
let indexB = this.element.bubbles.findIndex((bubble) => bubble.axis == swap[1])
|
||||
let dirA = this.element.bubbles[indexA].direction.clone();
|
||||
let dirB = this.element.bubbles[indexB].direction.clone();
|
||||
this.element.bubbles[indexA].direction.copy(dirB);
|
||||
this.element.bubbles[indexB].direction.copy(dirA);
|
||||
}
|
||||
// Append and listen for events
|
||||
this.element.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => {
|
||||
let lookFrom = scene.getCamera().position.clone();
|
||||
let lookAt = scene.getTarget().clone().add(scene.target.position);
|
||||
let magnitude = lookFrom.clone().sub(lookAt).length()
|
||||
let direction = new THREE.Vector3(axis.direction.x, axis.direction.y, axis.direction.z);
|
||||
let newLookFrom = lookAt.clone().add(direction.clone().multiplyScalar(magnitude));
|
||||
//console.log("New camera position", newLookFrom)
|
||||
scene.getCamera().position.copy(newLookFrom);
|
||||
scene.getCamera().lookAt(lookAt);
|
||||
scene.queueRender();
|
||||
}
|
||||
}
|
||||
|
||||
install() {
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.element.update();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// These are the default values for the settings, which are overridden below
|
||||
export const settings = {
|
||||
// @ts-ignore
|
||||
preloadModels: [new URL('../assets/fox.glb', import.meta.url).href, "ws://localhost:8080/api/updates"],
|
||||
preloadModels: [new URL('../../assets/fox.glb', import.meta.url).href, new URL('../../assets/logo.glbs', import.meta.url).href, "ws://localhost:8080"],
|
||||
// ModelViewer settings
|
||||
autoplay: true,
|
||||
arModes: 'webxr scene-viewer quick-look',
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {settings} from "./settings";
|
||||
import {settings} from "../tools/settings";
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {$scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||
@@ -11,14 +11,17 @@ let _ = ModelViewerElement; // HACK: Needed to avoid tree shaking
|
||||
|
||||
const emit = defineEmits(['load-viewer']);
|
||||
|
||||
const props = defineProps({
|
||||
src: String
|
||||
});
|
||||
|
||||
let viewer = ref<ModelViewerElement | null>(null);
|
||||
onMounted(() => {
|
||||
console.log('ModelViewerWrapper mounted');
|
||||
viewer.value.addEventListener('load', () =>
|
||||
emit('load-viewer', {
|
||||
viewer: viewer.value,
|
||||
scene: viewer.value[$scene] as ModelScene,
|
||||
})
|
||||
} as ModelViewerInfo)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,7 +30,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<!--suppress VueMissingComponentImportInspection -->
|
||||
<model-viewer ref="viewer"
|
||||
style="width: 100%; height: 100%" :src="settings.preloadModels[0]" 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" max-camera-orbit="Infinity 180deg auto"
|
||||
min-camera-orbit="-Infinity 0deg auto"
|
||||
:exposure="settings.exposure" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none"
|
||||
@@ -23,7 +23,7 @@ show_object = show
|
||||
def _get_app() -> web.Application:
|
||||
"""Required by aiohttp-devtools"""
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
from logo.logo import build_logo
|
||||
from logo import build_logo
|
||||
server.show_cad(build_logo(), 'logo')
|
||||
return server.app
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
|
||||
async def glb_sequence_to_glbs(glb_sequence: AsyncGenerator[bytes, None]) -> AsyncGenerator[bytes, None]:
|
||||
async def glb_sequence_to_glbs(glb_sequence: AsyncGenerator[bytes, None], count: int = -1) -> AsyncGenerator[bytes, None]:
|
||||
"""Converts a sequence of GLB files into a single GLBS file.
|
||||
|
||||
This is a streaming response in the custom GLBS format which consists of the "GLBS" magic text followed by
|
||||
a sequence of GLB files, each with a 4-byte little-endian length prefix."""
|
||||
a count of GLB files (0xffffffff if unknown) and a sequence of GLB files, each with a length prefix. All numbers are
|
||||
4-byte little-endian unsigned integers."""
|
||||
|
||||
# Write the magic text
|
||||
yield b'GLBS'
|
||||
|
||||
# Write the count
|
||||
yield count.to_bytes(4, 'little')
|
||||
|
||||
# Write the GLB files
|
||||
async for glb in glb_sequence:
|
||||
# Write the length prefix
|
||||
@@ -26,7 +30,7 @@ if __name__ == '__main__':
|
||||
yield b'glb00001'
|
||||
yield b'glb2'
|
||||
|
||||
async for chunk in glb_sequence_to_glbs(glb_sequence()):
|
||||
async for chunk in glb_sequence_to_glbs(glb_sequence(), 2):
|
||||
print(chunk)
|
||||
|
||||
asyncio.run(test_glb_sequence_to_glbs())
|
||||
@@ -202,7 +202,7 @@ class Server:
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
# noinspection PyTypeChecker
|
||||
export_data = tqdm.asyncio.tqdm(export_data, total=total_parts)
|
||||
async for chunk in glb_sequence_to_glbs(export_data):
|
||||
async for chunk in glb_sequence_to_glbs(export_data, total_parts):
|
||||
await response.write(chunk)
|
||||
finally:
|
||||
# Close the export data subscription
|
||||
@@ -308,7 +308,7 @@ class Server:
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
# noinspection PyTypeChecker
|
||||
glbs_parts = tqdm.asyncio.tqdm(glbs_parts, total=total_export_size, position=0)
|
||||
glbs_parts = glb_sequence_to_glbs(glbs_parts)
|
||||
glbs_parts = glb_sequence_to_glbs(glbs_parts, total_export_size)
|
||||
async for glbs_part in glbs_parts:
|
||||
yield glbs_part
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user