mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
started working on tools and sharing scene
This commit is contained in:
27
src/App.vue
27
src/App.vue
@@ -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>
|
||||
|
||||
@@ -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%">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
70
src/OrientationGizmo.vue
Normal 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>
|
||||
@@ -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
36
src/Tools.vue
Normal 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>
|
||||
12
src/index.ts
12
src/index.ts
@@ -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
5
src/shims.d.ts
vendored
@@ -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'
|
||||
Reference in New Issue
Block a user