mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 06:27:04 +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">
|
<script setup lang="ts">
|
||||||
import {defineAsyncComponent, ref} from "vue";
|
import {defineAsyncComponent, ref, Ref} from "vue";
|
||||||
import Sidebar from "./Sidebar.vue";
|
import Sidebar from "./Sidebar.vue";
|
||||||
import Loading from "./Loading.vue";
|
import Loading from "./Loading.vue";
|
||||||
import ModelViewerOverlay from "./ModelViewerOverlay.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
|
// NOTE: The ModelViewer library is big, so we split it and import it asynchronously
|
||||||
const ModelViewerWrapper = defineAsyncComponent({
|
const ModelViewerWrapper = defineAsyncComponent({
|
||||||
@@ -12,18 +23,22 @@ const ModelViewerWrapper = defineAsyncComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open models by default on wide screens
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-layout full-height>
|
<v-layout full-height>
|
||||||
<!-- 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/>
|
<model-viewer-wrapper @load-viewer="(args) => {
|
||||||
|
console.log('Model-Viewer finished loading:', args)
|
||||||
|
modelViewerInfo = args
|
||||||
|
}"/>
|
||||||
<model-viewer-overlay/>
|
<model-viewer-overlay/>
|
||||||
</v-main>
|
</v-main>
|
||||||
<!-- The left collapsible sidebar has the list of models -->
|
<!-- The left collapsible sidebar has the list of models -->
|
||||||
<sidebar :opened-init="openSidebars" side="left">
|
<sidebar :opened-init="openSidebarsDefault" side="left">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<v-toolbar-title>Models</v-toolbar-title>
|
<v-toolbar-title>Models</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,11 +52,11 @@ let openSidebars = ref(window.innerWidth > 1200);
|
|||||||
</v-expansion-panels>
|
</v-expansion-panels>
|
||||||
</sidebar>
|
</sidebar>
|
||||||
<!-- The right collapsible sidebar has the list of tools -->
|
<!-- 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>
|
<template #toolbar>
|
||||||
<v-toolbar-title>Tools</v-toolbar-title>
|
<v-toolbar-title>Tools</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
<v-btn>Camera</v-btn>
|
<tools :modelViewerInfo="modelViewerInfo"/>
|
||||||
</sidebar>
|
</sidebar>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {VContainer, VRow, VCol, VProgressCircular} from "vuetify/lib/components";
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-row justify="center" style="height: 100%">
|
<v-row justify="center" style="height: 100%">
|
||||||
|
|||||||
@@ -4,16 +4,29 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
<div class="overlay-svg-wrapper">
|
||||||
<!-- <rect x="0" y="0" width="100%" height="100%" fill="transparent"/>-->
|
<svg class="overlay-svg" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||||
</svg>
|
<!-- <rect x="0" y="0" width="100%" height="100%" fill="transparent"/>-->
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.overlay-svg-wrapper {
|
||||||
|
position: relative;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
.overlay-svg {
|
.overlay-svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {settings} from "./settings";
|
import {settings} from "./settings";
|
||||||
import {onMounted, ref} from "vue";
|
|
||||||
import {ModelViewerElement} from '@google/model-viewer';
|
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 {$scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||||
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
|
|
||||||
let _ = ModelViewerElement // HACK: Keep the import from being removed by the bundler
|
export type ModelViewerInfo = { viewer: ModelViewerElement, scene: ModelScene };
|
||||||
const viewer = ref(null);
|
|
||||||
|
let _ = ModelViewerElement; // HACK: Needed to avoid tree shaking
|
||||||
|
|
||||||
|
const emit = defineEmits(['load-viewer']);
|
||||||
|
|
||||||
|
let viewer = ref<ModelViewerElement | null>(null);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// TODO: Custom gizmo component inside tools sidebar
|
console.log('ModelViewerWrapper mounted');
|
||||||
// Gizmo installation
|
viewer.value.addEventListener('load', () =>
|
||||||
let scene: ModelScene = viewer.value[$scene];
|
emit('load-viewer', {
|
||||||
let gizmo = new OrientationGizmo(scene);
|
viewer: viewer.value,
|
||||||
gizmo.install();
|
scene: viewer.value[$scene] as ModelScene,
|
||||||
|
})
|
||||||
function updateGizmo() {
|
);
|
||||||
gizmo.update();
|
|
||||||
requestAnimationFrame(updateGizmo);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGizmo();
|
|
||||||
|
|
||||||
// TODO: Swap camera ortho/perspective tool
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<model-viewer
|
<!--suppress VueMissingComponentImportInspection -->
|
||||||
ref="viewer" style="width: 100%; height: 100%" :src="settings.preloadModels[0]" alt="The 3D model(s)" camera-controls
|
<model-viewer ref="viewer"
|
||||||
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto" min-camera-orbit="-Infinity 0deg auto"
|
style="width: 100%; height: 100%" :src="settings.preloadModels[0]" alt="The 3D model(s)" camera-controls
|
||||||
:exposure="settings.exposure" :shadow-intensity="settings.shadowIntensity" interaction-prompt="none"
|
camera-orbit="30deg 75deg auto" max-camera-orbit="Infinity 180deg auto"
|
||||||
:autoplay="settings.autoplay" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes"
|
min-camera-orbit="-Infinity 0deg auto"
|
||||||
:skybox-image="settings.background" :environment-image="settings.background"></model-viewer>
|
: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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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">
|
<script setup lang="ts">
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
|
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
openedInit: Boolean,
|
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-btn :icon="openIcon" @click="opened = !opened" class="open-button" :class="side"/>
|
||||||
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width">
|
<v-navigation-drawer v-model="opened" permanent :location="side" :width="props.width">
|
||||||
<v-toolbar density="compact">
|
<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>
|
<slot name="toolbar"></slot>
|
||||||
<v-toolbar-items>
|
<v-toolbar-items v-if="side == 'left'">
|
||||||
<slot name="toolbar-items"></slot>
|
<slot name="toolbar-items"></slot>
|
||||||
<v-btn icon="$close" @click="opened = !opened"/>
|
<v-btn icon="$close" @click="opened = !opened"/>
|
||||||
</v-toolbar-items>
|
</v-toolbar-items>
|
||||||
@@ -26,6 +31,7 @@ const openIcon = props.side === 'left' ? '$next' : '$prev';
|
|||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!--suppress CssUnusedSymbol -->
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.open-button {
|
.open-button {
|
||||||
position: absolute;
|
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 {createApp} from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import {createVuetify} from 'vuetify';
|
||||||
import 'vuetify/lib/styles/main.sass';
|
import 'vuetify/lib/styles/main.sass';
|
||||||
import { createVuetify } from 'vuetify';
|
|
||||||
import '@mdi/font/css/materialdesignicons.css'
|
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';
|
import * as directives from 'vuetify/lib/directives';
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
components,
|
|
||||||
directives,
|
directives,
|
||||||
theme: {
|
theme: {
|
||||||
defaultTheme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
defaultTheme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
||||||
@@ -28,4 +21,5 @@ const vuetify = createVuetify({
|
|||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(vuetify)
|
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
|
// Avoids typescript error when importing files
|
||||||
declare module '*.vue'
|
declare module '*.vue'
|
||||||
declare module 'import.meta' {
|
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
||||||
const url: string
|
|
||||||
export default url
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user