started working on tools and sharing scene

This commit is contained in:
Yeicor
2024-02-10 18:35:38 +01:00
parent 21947d3cef
commit 85a401e9ec
9 changed files with 182 additions and 47 deletions

View File

@@ -1,8 +1,19 @@
<script setup lang="ts">
import {defineAsyncComponent, ref} from "vue";
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";
// NOTE: The ModelViewer library is big, so we split it and import it asynchronously
const ModelViewerWrapper = defineAsyncComponent({
@@ -12,18 +23,22 @@ const ModelViewerWrapper = defineAsyncComponent({
});
// Open models by default on wide screens
let openSidebars = ref(window.innerWidth > 1200);
let openSidebarsDefault: Ref<boolean> = ref(window.innerWidth > 1200);
let modelViewerInfo: Ref<typeof ModelViewerInfo | null> = ref(null);
</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/>
<model-viewer-wrapper @load-viewer="(args) => {
console.log('Model-Viewer finished loading:', args)
modelViewerInfo = args
}"/>
<model-viewer-overlay/>
</v-main>
<!-- The left collapsible sidebar has the list of models -->
<sidebar :opened-init="openSidebars" side="left">
<sidebar :opened-init="openSidebarsDefault" side="left">
<template #toolbar>
<v-toolbar-title>Models</v-toolbar-title>
</template>
@@ -37,11 +52,11 @@ let openSidebars = ref(window.innerWidth > 1200);
</v-expansion-panels>
</sidebar>
<!-- The right collapsible sidebar has the list of tools -->
<sidebar :opened-init="openSidebars" side="right" width="150">
<sidebar :opened-init="openSidebarsDefault" side="right" :width="120">
<template #toolbar>
<v-toolbar-title>Tools</v-toolbar-title>
</template>
<v-btn>Camera</v-btn>
<tools :modelViewerInfo="modelViewerInfo"/>
</sidebar>
</v-layout>
</template>

View File

@@ -1,3 +1,7 @@
<script setup lang="ts">
import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components";
</script>
<template>
<v-container>
<v-row justify="center" style="height: 100%">

View File

@@ -4,16 +4,29 @@
</script>
<template>
<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 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: 100vh;
pointer-events: none;
}
</style>

View File

@@ -1,38 +1,38 @@
<script setup lang="ts">
import {settings} from "./settings";
import {onMounted, ref} from "vue";
import {ModelViewerElement} from '@google/model-viewer';
import {OrientationGizmo} from "./orientation";
import {onMounted, ref} from "vue";
import {$scene} from "@google/model-viewer/lib/model-viewer-base";
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
let _ = ModelViewerElement // HACK: Keep the import from being removed by the bundler
const viewer = ref(null);
export type ModelViewerInfo = { viewer: ModelViewerElement, scene: ModelScene };
let _ = ModelViewerElement; // HACK: Needed to avoid tree shaking
const emit = defineEmits(['load-viewer']);
let viewer = ref<ModelViewerElement | null>(null);
onMounted(() => {
// TODO: Custom gizmo component inside tools sidebar
// Gizmo installation
let scene: ModelScene = viewer.value[$scene];
let gizmo = new OrientationGizmo(scene);
gizmo.install();
function updateGizmo() {
gizmo.update();
requestAnimationFrame(updateGizmo);
}
updateGizmo();
// TODO: Swap camera ortho/perspective tool
console.log('ModelViewerWrapper mounted');
viewer.value.addEventListener('load', () =>
emit('load-viewer', {
viewer: viewer.value,
scene: viewer.value[$scene] as ModelScene,
})
);
});
</script>
<template>
<model-viewer
ref="viewer" style="width: 100%; height: 100%" :src="settings.preloadModels[0]" 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"
:autoplay="settings.autoplay" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes"
:skybox-image="settings.background" :environment-image="settings.background"></model-viewer>
<!--suppress VueMissingComponentImportInspection -->
<model-viewer ref="viewer"
style="width: 100%; height: 100%" :src="settings.preloadModels[0]" 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"
:autoplay="settings.autoplay" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes"
:skybox-image="settings.background" :environment-image="settings.background"></model-viewer>
</template>
<style scoped>

70
src/OrientationGizmo.vue Normal file
View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import {onMounted, onUpdated, ref} from "vue";
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
import * as THREE from "three";
globalThis.THREE = THREE // HACK: Required for the gizmo to work
const props = defineProps({
scene: ModelScene
});
function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElement {
// noinspection SpellCheckingInspection
let gizmo = new OrientationGizmoRaw(scene.camera, {
size: expectedParent.clientWidth,
bubbleSizePrimary: expectedParent.clientWidth / 12,
bubbleSizeSeconday: expectedParent.clientWidth / 14,
fontSize: (expectedParent.clientWidth / 10) + "px"
});
// HACK: Swap axes to match A-Frame
for (let swap of [["y", "-z"], ["z", "-y"], ["z", "-z"]]) {
let indexA = gizmo.bubbles.findIndex((bubble) => bubble.axis == swap[0])
let indexB = gizmo.bubbles.findIndex((bubble) => bubble.axis == swap[1])
let dirA = gizmo.bubbles[indexA].direction.clone();
let dirB = gizmo.bubbles[indexB].direction.clone();
gizmo.bubbles[indexA].direction.copy(dirB);
gizmo.bubbles[indexB].direction.copy(dirA);
}
// Append and listen for events
gizmo.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();
}
return gizmo;
}
// Mount, unmount and listen for scene changes
let container = ref<HTMLElement | null>(null);
let gizmo: HTMLElement & { update: () => void }
function updateGizmo() {
if (gizmo.isConnected) {
gizmo.update();
requestIdleCallback(updateGizmo);
}
}
let reinstall = () => {
if (gizmo) container.value.removeChild(gizmo);
gizmo = createGizmo(container.value, props.scene) as typeof gizmo;
container.value.appendChild(gizmo);
requestIdleCallback(updateGizmo);
}
onMounted(reinstall)
onUpdated(reinstall);
// onUnmounted is not needed because the gizmo is removed when the container is removed
</script>
<template>
<div ref="container" class="orientation-gizmo"/>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import {ref} from "vue";
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components";
const props = defineProps({
openedInit: Boolean,
@@ -16,8 +17,12 @@ const openIcon = props.side === 'left' ? '$next' : '$prev';
<v-btn :icon="openIcon" @click="opened = !opened" class="open-button" :class="side"/>
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width">
<v-toolbar density="compact">
<v-toolbar-items v-if="side == 'right'">
<slot name="toolbar-items"></slot>
<v-btn icon="$close" @click="opened = !opened"/>
</v-toolbar-items>
<slot name="toolbar"></slot>
<v-toolbar-items>
<v-toolbar-items v-if="side == 'left'">
<slot name="toolbar-items"></slot>
<v-btn icon="$close" @click="opened = !opened"/>
</v-toolbar-items>
@@ -26,6 +31,7 @@ const openIcon = props.side === 'left' ? '$next' : '$prev';
</v-navigation-drawer>
</template>
<!--suppress CssUnusedSymbol -->
<style scoped>
.open-button {
position: absolute;

36
src/Tools.vue Normal file
View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import {VBtn, VIcon} from "vuetify/lib/components";
import OrientationGizmo from "./OrientationGizmo.vue";
const props = defineProps({
modelViewerInfo: Object
});
function toggleProjection() {
if (!props.modelViewerInfo) return;
}
</script>
<template>
<orientation-gizmo v-if="props.modelViewerInfo" :scene="props.modelViewerInfo.scene"/>
<v-btn icon="mdi-projector" @click="toggleProjection"><span class="icon-detail">PERSP</span>
<v-icon icon="mdi-projector"></v-icon>
</v-btn>
</template>
<style>
.icon-detail {
position: absolute;
top: 10px;
left: 0;
font-size: xx-small;
width: 100%;
margin: auto;
}
.icon-detail + .v-icon {
position: relative;
top: 5px;
}
</style>

View File

@@ -7,19 +7,12 @@ globalThis.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = process.env.NODE_ENV === "d
import {createApp} from 'vue'
import App from './App.vue'
import {createVuetify} from 'vuetify';
import 'vuetify/lib/styles/main.sass';
import { createVuetify } from 'vuetify';
import '@mdi/font/css/materialdesignicons.css'
// TODO: Only import the components and directives that are actually used
// @ts-ignore
import * as components from 'vuetify/lib/components';
// @ts-ignore
import * as directives from 'vuetify/lib/directives';
const vuetify = createVuetify({
components,
directives,
theme: {
defaultTheme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
@@ -28,4 +21,5 @@ const vuetify = createVuetify({
const app = createApp(App)
app.use(vuetify)
app.mount('body')
// noinspection JSUnresolvedReference
app.mount('body')

5
src/shims.d.ts vendored
View File

@@ -1,6 +1,3 @@
// Avoids typescript error when importing files
declare module '*.vue'
declare module 'import.meta' {
const url: string
export default url
}
declare module 'three-orientation-gizmo/src/OrientationGizmo'