fix(frontend): embed viewer bugfixes & speed improvements
This commit is contained in:
+2
-1
@@ -36,7 +36,8 @@
|
||||
"resolutions": {
|
||||
"tslib": "^2.3.1",
|
||||
"core-js": "3.22.4",
|
||||
"vue-cli-plugin-apollo/graphql": "^15"
|
||||
"vue-cli-plugin-apollo/graphql": "^15",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@rushstack/eslint-patch": "^1.1.3",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/mixpanel-browser": "^2.38.0",
|
||||
"@types/node": "^17.0.43",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"@vue/cli": "^4.5.17",
|
||||
|
||||
@@ -1,138 +1,66 @@
|
||||
<template>
|
||||
<!--
|
||||
HIC SVNT DRACONES
|
||||
this needs some cleanup, possibly even moving to the main app,
|
||||
and ensuring local storage absence handling is properly done
|
||||
-->
|
||||
<v-app
|
||||
:class="`no-scrollbar ${
|
||||
:class="`embed-viewer no-scrollbar ${
|
||||
$vuetify.theme.dark ? 'background-dark' : 'background-light'
|
||||
}`"
|
||||
>
|
||||
<div v-if="!error" style="z-index: 10">
|
||||
<div
|
||||
class="top-left bottom-left pa-4"
|
||||
style="right: 0px; position: fixed; z-index: 100000"
|
||||
>
|
||||
<span v-show="!drawer" class="caption d-inline-flex align-center">
|
||||
<img src="@/assets/logo.svg" height="18" />
|
||||
<span style="margin-top: 2px" class="primary--text">
|
||||
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
|
||||
<b>Powered by Speckle</b>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
<div v-show="!drawer && loadedModel" class="caption grey--text pa-2">
|
||||
<v-btn fab small @click="drawer = true">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
class="pa-2 d-flex align-center justify-space-between caption"
|
||||
style="position: fixed; bottom: 0; width: 100%"
|
||||
>
|
||||
<portal to="viewercontrols">
|
||||
<v-btn
|
||||
v-if="stream && serverInfo"
|
||||
v-tooltip="'View extra details in Speckle!'"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
class="elevation-5 primary pa-0 ma-o"
|
||||
:href="goToServerUrl"
|
||||
target="blank"
|
||||
>
|
||||
<v-icon dark small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</portal>
|
||||
</div>
|
||||
<div
|
||||
:style="`width: 100%; bottom: 12px; left: 0px; position: ${
|
||||
$isMobile() ? 'fixed' : 'absolute'
|
||||
}; z-index: 20`"
|
||||
:class="`d-flex justify-center`"
|
||||
>
|
||||
<viewer-controls v-show="loadedModel" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BG image -->
|
||||
<div
|
||||
v-if="!loadedModel"
|
||||
v-if="!isModelLoaded"
|
||||
ref="cover"
|
||||
class="d-flex fullscreen align-center justify-center bg-img"
|
||||
class="viewer-image-overlay d-flex fullscreen align-center justify-center bg-img"
|
||||
/>
|
||||
|
||||
<!-- Play button -->
|
||||
<div
|
||||
v-if="!loadedModel && loadProgress > 0"
|
||||
class="d-flex fullscreen align-center justify-center"
|
||||
v-if="!isModelLoaded"
|
||||
class="viewer-play d-flex fullscreen align-center justify-center"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-model="loadProgress"
|
||||
:indeterminate="loadProgress >= 99 && !loadedModel"
|
||||
<v-btn
|
||||
id="viewer-play-btn"
|
||||
:disabled="showPlayLoader"
|
||||
fab
|
||||
color="primary"
|
||||
style="max-width: 30%"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadedModel && loadProgress === 0"
|
||||
class="d-flex fullscreen align-center justify-center"
|
||||
>
|
||||
<v-btn fab color="primary" class="elevation-20 hover-tada" @click="load()">
|
||||
<v-icon>mdi-play</v-icon>
|
||||
class="elevation-4 hover-tada"
|
||||
@click="load()"
|
||||
>
|
||||
<v-icon v-if="!showPlayLoader">mdi-play</v-icon>
|
||||
<v-icon v-else class="spinning-icon">mdi-loading</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-navigation-drawer
|
||||
ref="drawer"
|
||||
v-model="drawer"
|
||||
app
|
||||
floating
|
||||
style="z-index: 10000"
|
||||
>
|
||||
<div class="mx-1 mt-4 pr-2" style="height: 100%; width: 100%">
|
||||
<!-- Views display -->
|
||||
<views-display v-if="views.length !== 0" :views="views" :sticky-top="false" />
|
||||
|
||||
<!-- Filters display -->
|
||||
<viewer-filters
|
||||
:props="objectProperties"
|
||||
style="width: 100%"
|
||||
:sticky-top="false"
|
||||
/>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
<div style="position: fixed" class="no-scrollbar">
|
||||
<speckle-viewer @load-progress="captureProgress" />
|
||||
</div>
|
||||
<!-- Async loaded viewer -->
|
||||
<embed-viewer-core
|
||||
v-if="shouldLoadHeavyDeps"
|
||||
:input="input"
|
||||
:object-url="objectUrl"
|
||||
@model-loaded="onModelLoaded"
|
||||
@error="onError"
|
||||
/>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue'
|
||||
import { getCommit, getLatestBranchCommit, getServerInfo } from '@/embed/speckleUtils'
|
||||
<script lang="ts">
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import { getCommit, getLatestBranchCommit } from '@/embed/speckleUtils'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
/**
|
||||
* TODO:
|
||||
* - Move EmbedViewer back to main app? The main app has a lot of global dependencies
|
||||
* that this endpoint doesn't need, tho...
|
||||
* - Make speckle-viewer configurable through props/events not through window.__viewer
|
||||
*/
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'EmbedViewer',
|
||||
components: {
|
||||
SpeckleViewer,
|
||||
ViewerControls: () => import('@/main/components/viewer/ViewerControls'),
|
||||
ViewsDisplay: () => import('@/main/components/viewer/ViewsDisplay'),
|
||||
ViewerFilters: () => import('@/main/components/viewer/ViewerFilters.vue')
|
||||
},
|
||||
filters: {
|
||||
truncate(str, n = 20) {
|
||||
return str.length > n ? str.substr(0, n - 3) + '...' : str
|
||||
}
|
||||
EmbedViewerCore: () => import('@/embed/EmbedViewerCore.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
loadedModel: false,
|
||||
loadProgress: 0,
|
||||
error: null,
|
||||
objectId: this.$route.query.object,
|
||||
views: [],
|
||||
objectProperties: null,
|
||||
isModelLoaded: false,
|
||||
error: null as Nullable<Error>,
|
||||
input: {
|
||||
stream: this.$route.query.stream,
|
||||
object: this.$route.query.object,
|
||||
@@ -141,19 +69,13 @@ export default {
|
||||
overlay: this.$route.query.overlay,
|
||||
camera: this.$route.query.c,
|
||||
filter: this.$route.query.filter
|
||||
},
|
||||
lastCommit: null,
|
||||
specificCommit: null,
|
||||
serverInfo: null
|
||||
} as Record<string, string>,
|
||||
isInitialized: false as boolean,
|
||||
shouldLoadHeavyDeps: false as boolean
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSmall() {
|
||||
return (
|
||||
this.$vuetify.breakpoint.name === 'xs' || this.$vuetify.breakpoint.name === 'sm'
|
||||
)
|
||||
},
|
||||
displayType() {
|
||||
displayType(): string {
|
||||
if (!this.input.stream) {
|
||||
return 'error'
|
||||
}
|
||||
@@ -164,150 +86,95 @@ export default {
|
||||
|
||||
return 'stream'
|
||||
},
|
||||
stream() {
|
||||
return this.lastCommit || this.specificCommit
|
||||
objectUrl(): string {
|
||||
return `${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${this.input.object}`
|
||||
},
|
||||
objectUrl() {
|
||||
return `${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${this.objectId}`
|
||||
},
|
||||
goToServerUrl() {
|
||||
const stream = this.input.stream
|
||||
const base = `${window.location.origin}/streams/${stream}/`
|
||||
|
||||
const commit = this.input.commit
|
||||
if (commit) return base + `commits/${commit}`
|
||||
|
||||
const object = this.objectId
|
||||
if (object) return base + `objects/${object}`
|
||||
|
||||
const branch = this.input.branch
|
||||
if (branch) return base + `branches/${encodeURI(branch)}`
|
||||
|
||||
return base
|
||||
showPlayLoader(): boolean {
|
||||
return !this.isInitialized || (this.shouldLoadHeavyDeps && !this.isModelLoaded)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
displayType(oldVal, newVal) {
|
||||
if (newVal === 'error') this.error = 'Provided details were invalid'
|
||||
else {
|
||||
this.error = null
|
||||
}
|
||||
displayType(_oldVal: string, newVal: string) {
|
||||
this.error =
|
||||
newVal === 'error' ? new Error('Provided details were invalid') : null
|
||||
},
|
||||
error(newVal: Error | null) {
|
||||
if (newVal) console.error(newVal)
|
||||
}
|
||||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
const serverInfoResponse = await getServerInfo()
|
||||
this.serverInfo = serverInfoResponse.data.serverInfo
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
return
|
||||
}
|
||||
if (this.displayType === 'commit') {
|
||||
try {
|
||||
const res = await getCommit(this.input.stream, this.input.commit)
|
||||
const data = res.data
|
||||
const latestCommit = data.stream.commit
|
||||
if (this.input.object === undefined)
|
||||
this.objectId = latestCommit.referencedObject
|
||||
this.specificCommit = data.stream
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
return
|
||||
// Initialize base data, which can potentially change input.object
|
||||
await (this.displayType === 'commit'
|
||||
? this.initializeForCommit()
|
||||
: this.initializeForStream())
|
||||
|
||||
// Load BG image
|
||||
await this.getPreviewImage()
|
||||
|
||||
// Mark as initialized (enable play button)
|
||||
this.isInitialized = true
|
||||
},
|
||||
methods: {
|
||||
onError(e: Error) {
|
||||
this.error = e
|
||||
},
|
||||
onModelLoaded() {
|
||||
this.isModelLoaded = true
|
||||
},
|
||||
load() {
|
||||
if (!this.isInitialized || this.shouldLoadHeavyDeps || this.isModelLoaded) return
|
||||
|
||||
this.shouldLoadHeavyDeps = true
|
||||
this.$mixpanel.track('Embedded Model Load', {
|
||||
type: 'action'
|
||||
})
|
||||
},
|
||||
async getPreviewImage(angle?: number) {
|
||||
angle = angle || 0
|
||||
const previewUrl = this.objectUrl.replace('streams', 'preview') + '/' + angle
|
||||
const res = await fetch(previewUrl)
|
||||
const blob = await res.blob()
|
||||
const imgUrl = URL.createObjectURL(blob)
|
||||
if (this.$refs.cover) {
|
||||
;(this.$refs.cover as HTMLElement).style.backgroundImage = `url('${imgUrl}')`
|
||||
}
|
||||
} else {
|
||||
},
|
||||
async initializeForStream() {
|
||||
try {
|
||||
const res = await getLatestBranchCommit(this.input.stream, this.input.branch)
|
||||
const data = res.data
|
||||
const latestCommit =
|
||||
data.stream.branch.commits.items[0] || data.stream.branch.commit
|
||||
|
||||
if (!latestCommit) {
|
||||
this.error = 'No commit for this branch'
|
||||
this.lastCommit = data.stream
|
||||
this.error = new Error('No commit for this branch')
|
||||
return
|
||||
}
|
||||
if (this.input.object === undefined)
|
||||
this.objectId = latestCommit.referencedObject
|
||||
else this.objectId = this.input.object
|
||||
this.lastCommit = data.stream
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
console.log(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.getPreviewImage()
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
async load() {
|
||||
this.$mixpanel.track('Embedded Model Load', {
|
||||
step: this.onboarding,
|
||||
type: 'action'
|
||||
})
|
||||
|
||||
await window.__viewer.loadObject(this.objectUrl)
|
||||
|
||||
if (this.input.overlay) {
|
||||
const resIds = this.input.overlay.split(',')
|
||||
for (const res of resIds) {
|
||||
console.log(res)
|
||||
if (res.length !== 10) {
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${res}`
|
||||
)
|
||||
} else {
|
||||
const { data } = await getCommit(this.input.stream, res)
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${data.stream.commit.referencedObject}`
|
||||
)
|
||||
}
|
||||
// Updating input.object
|
||||
if (this.input.object === undefined) {
|
||||
this.input.object = latestCommit.referencedObject
|
||||
}
|
||||
}
|
||||
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
|
||||
this.loadedModel = true
|
||||
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
|
||||
if (this.input.filter) {
|
||||
const parsedFilter = JSON.parse(this.input.filter)
|
||||
setTimeout(() => {
|
||||
this.$store.commit('setFilterDirect', { filter: parsedFilter })
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (this.input.camera) {
|
||||
const cam = JSON.parse(this.input.camera)
|
||||
window.__viewer.interactions.setLookAt(
|
||||
{ x: cam[0], y: cam[1], z: cam[2] }, // position
|
||||
{ x: cam[3], y: cam[4], z: cam[5] } // target
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
this.error = e instanceof Error ? e : new Error('An unexpected error occurred')
|
||||
}
|
||||
},
|
||||
captureProgress(args) {
|
||||
this.loadProgress = args.progress * 100
|
||||
},
|
||||
async getPreviewImage(angle) {
|
||||
angle = angle || 0
|
||||
const previewUrl = this.objectUrl.replace('streams', 'preview') + '/' + angle
|
||||
let token = undefined
|
||||
async initializeForCommit() {
|
||||
try {
|
||||
token = localStorage.getItem('AuthToken')
|
||||
} catch (e) {
|
||||
console.warn('Sanboxed mode, only public streams will fetch properly.')
|
||||
const res = await getCommit(this.input.stream, this.input.commit)
|
||||
const data = res.data
|
||||
const latestCommit = data.stream.commit
|
||||
|
||||
// Updating input.object
|
||||
if (this.input.object === undefined) {
|
||||
this.input.object = latestCommit.referencedObject
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
this.error = e instanceof Error ? e : new Error('An unexpected error occurred')
|
||||
}
|
||||
const res = await fetch(previewUrl, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
})
|
||||
const blob = await res.blob()
|
||||
const imgUrl = URL.createObjectURL(blob)
|
||||
if (this.$refs.cover) this.$refs.cover.style.backgroundImage = `url('${imgUrl}')`
|
||||
this.hasImg = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -319,26 +186,51 @@ body::-webkit-scrollbar {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
position: fixed;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-img {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/*background-attachment: fixed;*/
|
||||
filter: blur(2px);
|
||||
}
|
||||
.no-events {
|
||||
pointer-events: none;
|
||||
|
||||
#viewer-play-btn {
|
||||
@keyframes spinner-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// kinda hacky, but vuetify renders the disabled state in a stupid way that relies
|
||||
// on the background color of the thing behind the button
|
||||
&.v-btn--fab.v-btn--disabled {
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
|
||||
.spinning-icon {
|
||||
animation: spinner-spin 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="embed-viewer-core">
|
||||
<!-- Viewer navbar (position fixed) -->
|
||||
<div v-if="!error" style="z-index: 10" class="viewer-navbar">
|
||||
<div
|
||||
class="top-left bottom-left pa-4"
|
||||
style="right: 0px; position: fixed; z-index: 100000"
|
||||
>
|
||||
<span v-show="!drawer" class="caption d-inline-flex align-center">
|
||||
<img src="@/assets/logo.svg" height="18" />
|
||||
<span style="margin-top: 2px" class="primary--text">
|
||||
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
|
||||
<b>Powered by Speckle</b>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
<div v-show="!drawer && loadedModel" class="caption grey--text pa-2">
|
||||
<v-btn fab small @click="drawer = true">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
class="pa-2 d-flex align-center justify-space-between caption"
|
||||
style="position: fixed; bottom: 0; width: 100%"
|
||||
>
|
||||
<portal to="viewercontrols">
|
||||
<v-btn
|
||||
v-tooltip="'View extra details in Speckle!'"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
class="elevation-5 primary pa-0 ma-o"
|
||||
:href="goToServerUrl"
|
||||
target="blank"
|
||||
>
|
||||
<v-icon dark small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</portal>
|
||||
</div>
|
||||
<div
|
||||
:style="`width: 100%; bottom: 12px; left: 0px; position: ${
|
||||
$isMobile() ? 'fixed' : 'absolute'
|
||||
}; z-index: 20`"
|
||||
:class="`d-flex justify-center`"
|
||||
>
|
||||
<viewer-controls v-show="loadedModel" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model loading progress bar -->
|
||||
<div
|
||||
v-if="!loadedModel && loadProgress > 0"
|
||||
class="viewer-loader d-flex fullscreen align-center justify-center"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-model="loadProgress"
|
||||
:indeterminate="loadProgress >= 99 && !loadedModel"
|
||||
color="primary"
|
||||
style="max-width: 30%"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
|
||||
<!-- Viewer filters panel / sidebar -->
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
class="viewer-controls-drawer"
|
||||
app
|
||||
floating
|
||||
style="z-index: 10000"
|
||||
>
|
||||
<div class="px-1 pt-1 d-flex flex-column" style="height: 100%; width: 100%">
|
||||
<!-- Drawer closer -->
|
||||
<v-btn icon small class="align-self-end mb-2" @click="drawer = false">
|
||||
<v-icon x-small>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Views display -->
|
||||
<views-display v-if="views.length !== 0" :views="views" :sticky-top="false" />
|
||||
|
||||
<!-- Filters display -->
|
||||
<viewer-filters
|
||||
:props="objectProperties"
|
||||
style="width: 100%"
|
||||
:sticky-top="false"
|
||||
/>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Actual viewer -->
|
||||
<div style="position: fixed" class="viewer-wrapper no-scrollbar">
|
||||
<speckle-viewer @load-progress="captureProgress" @viewer-init="onViewerInit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import Vue, { PropType } from 'vue'
|
||||
import { getCommit } from '@/embed/speckleUtils'
|
||||
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue'
|
||||
import ViewerControls from '@/main/components/viewer/ViewerControls.vue'
|
||||
import ViewsDisplay from '@/main/components/viewer/ViewsDisplay.vue'
|
||||
import ViewerFilters from '@/main/components/viewer/ViewerFilters.vue'
|
||||
|
||||
/**
|
||||
* Core embed viewer functionality with all of the heavy JS dependencies has been extracted to this component,
|
||||
* so that we can lazy-load it only when the user clicks on the "play" button
|
||||
*
|
||||
* Make sure this component isn't initialized until it's actually needed and don't put any heavy deps
|
||||
* inside EmbedViewer.vue (or asynchronize them)
|
||||
*/
|
||||
|
||||
type UnknownObject = Record<string, unknown>
|
||||
|
||||
type EmbedViewerInput = {
|
||||
stream: string
|
||||
object: Nullable<string>
|
||||
branch: Nullable<string>
|
||||
commit: Nullable<string>
|
||||
overlay: Nullable<string>
|
||||
camera: Nullable<string>
|
||||
filter: Nullable<string>
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'EmbedViewerCore',
|
||||
components: {
|
||||
SpeckleViewer,
|
||||
ViewerControls,
|
||||
ViewsDisplay,
|
||||
ViewerFilters
|
||||
},
|
||||
props: {
|
||||
input: {
|
||||
type: Object as PropType<EmbedViewerInput>,
|
||||
required: true
|
||||
},
|
||||
objectUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null as Nullable<Error>,
|
||||
drawer: false,
|
||||
loadedModel: false,
|
||||
loadProgress: 0,
|
||||
views: [] as Array<UnknownObject>,
|
||||
objectProperties: null as Nullable<UnknownObject>
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
goToServerUrl(): string {
|
||||
const stream = this.input.stream
|
||||
const base = `${window.location.origin}/streams/${stream}/`
|
||||
|
||||
const commit = this.input.commit
|
||||
if (commit) return base + `commits/${commit}`
|
||||
|
||||
const object = this.input.object
|
||||
if (object) return base + `objects/${object}`
|
||||
|
||||
const branch = this.input.branch
|
||||
if (branch) return base + `branches/${encodeURI(branch)}`
|
||||
|
||||
return base
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
error(newVal: Error | null) {
|
||||
if (newVal) this.$emit('error', newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
captureProgress(args: { progress: number; id: number; url: string }) {
|
||||
this.loadProgress = args.progress * 100
|
||||
},
|
||||
markModelLoaded() {
|
||||
this.loadedModel = true
|
||||
this.$emit('model-loaded')
|
||||
},
|
||||
async onViewerInit() {
|
||||
if (!window.__viewer) {
|
||||
throw new Error('Viewer instance unavailable')
|
||||
}
|
||||
|
||||
await window.__viewer.loadObject(this.objectUrl)
|
||||
|
||||
const overlayPromises = []
|
||||
if (this.input.overlay) {
|
||||
const resIds = this.input.overlay.split(',')
|
||||
for (const res of resIds) {
|
||||
if (res.length !== 10) {
|
||||
overlayPromises.push(
|
||||
window.__viewer.loadObject(
|
||||
`${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${res}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
overlayPromises.push(
|
||||
getCommit(this.input.stream, res).then(({ data }) => {
|
||||
return window.__viewer!.loadObject(
|
||||
`${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${data.stream.commit.referencedObject}`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(overlayPromises)
|
||||
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
|
||||
this.markModelLoaded()
|
||||
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
|
||||
if (this.input.filter) {
|
||||
const parsedFilter = JSON.parse(this.input.filter)
|
||||
setTimeout(() => {
|
||||
this.$store.commit('setFilterDirect', { filter: parsedFilter })
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (this.input.camera) {
|
||||
const cam = JSON.parse(this.input.camera)
|
||||
window.__viewer.interactions.setLookAt(
|
||||
{ x: cam[0], y: cam[1], z: cam[2] }, // position
|
||||
{ x: cam[3], y: cam[4], z: cam[5] } // target
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.embed-viewer-core {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.fullscreen {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
position: fixed;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import '@/vueBootstrapper'
|
||||
|
||||
import App from './EmbedApp.vue'
|
||||
import vuetify from './embedVuetify'
|
||||
import router from './embedRouter'
|
||||
|
||||
// process.env.NODE_ENV is injected by Webpack
|
||||
// eslint-disable-next-line no-undef
|
||||
Vue.config.productionTip = process.env.NODE_ENV === 'development'
|
||||
|
||||
import VueMixpanel from 'vue-mixpanel'
|
||||
Vue.use(VueMixpanel, {
|
||||
token: 'acd87c5a50b56df91a795e999812a3a4',
|
||||
config: {
|
||||
// eslint-disable-next-line camelcase
|
||||
api_host: 'https://analytics.speckle.systems'
|
||||
}
|
||||
})
|
||||
|
||||
import '@/plugins/helpers'
|
||||
import store from '@/main/store'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
Vue.use(PortalVue)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
// Event hub
|
||||
Vue.prototype.$eventHub = new Vue()
|
||||
import '@/vueBootstrapper'
|
||||
|
||||
import App from '@/main/App.vue'
|
||||
import store from '@/main/store'
|
||||
@@ -16,13 +14,6 @@ import {
|
||||
import router from '@/main/router/index'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
// process.env.NODE_ENV is injected by Webpack
|
||||
// eslint-disable-next-line no-undef
|
||||
Vue.config.productionTip = process.env.NODE_ENV === 'development'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
Vue.use(PortalVue)
|
||||
|
||||
import VueTimeago from 'vue-timeago'
|
||||
Vue.use(VueTimeago, { locale: 'en' })
|
||||
|
||||
@@ -37,22 +28,6 @@ import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
|
||||
|
||||
Vue.use(PerfectScrollbar)
|
||||
|
||||
import VTooltip from 'v-tooltip'
|
||||
Vue.use(VTooltip, {
|
||||
defaultDelay: 300,
|
||||
defaultBoundariesElement: document.body,
|
||||
defaultHtml: false
|
||||
})
|
||||
|
||||
import VueMixpanel from 'vue-mixpanel'
|
||||
Vue.use(VueMixpanel, {
|
||||
token: 'acd87c5a50b56df91a795e999812a3a4',
|
||||
config: {
|
||||
// eslint-disable-next-line camelcase
|
||||
api_host: 'https://analytics.speckle.systems'
|
||||
}
|
||||
})
|
||||
|
||||
// Async HistogramSlider load
|
||||
Vue.component('HistogramSlider', async () => {
|
||||
await import(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
declare module 'vue-mixpanel' {
|
||||
declare const test: string
|
||||
declare const plugin: import('vue').PluginFunction<unknown>
|
||||
export default plugin
|
||||
export { test }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
import Vue, { VNode } from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface Element extends VNode {}
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
declare module 'vue/types/vue' {
|
||||
export interface Vue {
|
||||
$mixpanel: import('mixpanel-browser').OverridedMixpanel
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -0,0 +1,10 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Initialized in SpeckleViewer.vue
|
||||
*/
|
||||
__viewer?: import('@speckle/viewer').Viewer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Vue from 'vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
import VueMixpanel from 'vue-mixpanel'
|
||||
import PortalVue from 'portal-vue'
|
||||
|
||||
/**
|
||||
* Global Vue bootstrapping that is used in all of the frontend apps (main/embed)
|
||||
*/
|
||||
|
||||
// process.env.NODE_ENV is injected by Webpack
|
||||
Vue.config.productionTip = process.env.NODE_ENV === 'development'
|
||||
|
||||
Vue.use(VTooltip, {
|
||||
defaultDelay: 300,
|
||||
defaultBoundariesElement: document.body,
|
||||
defaultHtml: false
|
||||
})
|
||||
|
||||
Vue.use(VueMixpanel, {
|
||||
token: 'acd87c5a50b56df91a795e999812a3a4',
|
||||
config: {
|
||||
// eslint-disable-next-line camelcase
|
||||
api_host: 'https://analytics.speckle.systems'
|
||||
}
|
||||
})
|
||||
|
||||
Vue.use(PortalVue)
|
||||
|
||||
// Event hub
|
||||
Vue.prototype.$eventHub = new Vue()
|
||||
@@ -11,6 +11,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
@@ -21,7 +22,10 @@
|
||||
"vue-apollo-smart-ops",
|
||||
"vue-infinite-loading",
|
||||
"type-fest",
|
||||
"vue"
|
||||
"vue",
|
||||
"@types/node",
|
||||
"@types/mixpanel-browser",
|
||||
"vuetify"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "PG_CONNECTION_STRING=postgresql://localhost:5432/speckle2_dev DEBUG='preview-service:*' nodemon --trace-deprecation ./bin/www",
|
||||
"dev": "DEBUG='preview-service:*' nodemon --trace-deprecation ./bin/www",
|
||||
"build": "webpack --env dev --config webpack.config.render_page.js && webpack --env build --config webpack.config.render_page.js",
|
||||
"lint": "eslint . --ext .js,.ts"
|
||||
},
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": "3",
|
||||
"targets": {
|
||||
"node": "12"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"ignore": ["node_modules/**/*"]
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* Extends repo root config, only put changes here that are scoped to this specific package
|
||||
* (if you're already are - evaluate whether you really need package scoped linting rules)
|
||||
@@ -8,8 +10,12 @@ const config = {
|
||||
env: {
|
||||
browser: true
|
||||
},
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
sourceType: 'module',
|
||||
babelOptions: {
|
||||
configFile: path.resolve(__dirname, './babel.config.js')
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3',
|
||||
targets: {
|
||||
node: '11'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
ignore: ['node_modules/**/*']
|
||||
}
|
||||
@@ -49,6 +49,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@types/three": "^0.136.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
|
||||
@@ -13,10 +13,11 @@ import { getConversionFactor } from './converter/Units'
|
||||
* Manages objects and provides some convenience methods to focus on the entire scene, or one specific object.
|
||||
*/
|
||||
export default class SceneObjectManager {
|
||||
views = []
|
||||
|
||||
constructor(viewer, skipPostLoad = false) {
|
||||
this.viewer = viewer
|
||||
this.scene = viewer.scene
|
||||
this.views = []
|
||||
|
||||
this.sceneObjects = new SceneObjects(viewer)
|
||||
|
||||
|
||||
@@ -270,11 +270,7 @@ export class Viewer extends EventEmitter implements IViewer {
|
||||
this.cameraHandler.toggleCameras()
|
||||
}
|
||||
|
||||
public async loadObject(
|
||||
url: string,
|
||||
token: string | undefined,
|
||||
enableCaching = true
|
||||
) {
|
||||
public async loadObject(url: string, token?: string, enableCaching = true) {
|
||||
try {
|
||||
if (++this.inProgressOperations === 1) (this as EventEmitter).emit('busy', true)
|
||||
|
||||
|
||||
@@ -310,6 +310,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/eslint-parser@npm:^7.18.2":
|
||||
version: 7.18.2
|
||||
resolution: "@babel/eslint-parser@npm:7.18.2"
|
||||
dependencies:
|
||||
eslint-scope: ^5.1.1
|
||||
eslint-visitor-keys: ^2.1.0
|
||||
semver: ^6.3.0
|
||||
peerDependencies:
|
||||
"@babel/core": ">=7.11.0"
|
||||
eslint: ^7.5.0 || ^8.0.0
|
||||
checksum: dc9328cf3304b25c9029682e6b6196761e18d3ab80d66c3085a69c6f240fa2db91b824a61672e94139e73683b7ceeefe9ff58acac1ee89fe73274007b16e43d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:7.17.10, @babel/generator@npm:^7.17.10":
|
||||
version: 7.17.10
|
||||
resolution: "@babel/generator@npm:7.17.10"
|
||||
@@ -3906,6 +3920,8 @@ __metadata:
|
||||
"@tiptap/vue-2": ^2.0.0-beta.79
|
||||
"@tryghost/content-api": ^1.5.12
|
||||
"@types/lodash": ^4.14.180
|
||||
"@types/mixpanel-browser": ^2.38.0
|
||||
"@types/node": ^17.0.43
|
||||
"@typescript-eslint/eslint-plugin": ^5.21.0
|
||||
"@typescript-eslint/parser": ^5.21.0
|
||||
"@vue/cli": ^4.5.17
|
||||
@@ -4134,6 +4150,7 @@ __metadata:
|
||||
resolution: "@speckle/viewer@workspace:packages/viewer"
|
||||
dependencies:
|
||||
"@babel/core": ^7.18.2
|
||||
"@babel/eslint-parser": ^7.18.2
|
||||
"@rollup/plugin-babel": ^5.3.1
|
||||
"@speckle/objectloader": "workspace:^"
|
||||
"@types/three": ^0.136.0
|
||||
@@ -4785,6 +4802,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mixpanel-browser@npm:^2.38.0":
|
||||
version: 2.38.0
|
||||
resolution: "@types/mixpanel-browser@npm:2.38.0"
|
||||
checksum: 1ade271188446005644cf10b65af554a9a75bd2008b827db4cdcfb2eedf858e2426a4d3d21ccf5b0a25a8590f8c3095da25549cfb7765077002e03e64afa9f0a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mocha@npm:^7.0.2":
|
||||
version: 7.0.2
|
||||
resolution: "@types/mocha@npm:7.0.2"
|
||||
@@ -4823,6 +4847,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^17.0.43":
|
||||
version: 17.0.43
|
||||
resolution: "@types/node@npm:17.0.43"
|
||||
checksum: 7fd84b1e37dc406c1c73a1461ccd6a1a38a5a335563788dd25e0aa75476d6948f815d0e9e4c2201f785a36a72269e6aaf0d851ca75c1e44b92e3a9d504e04a1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/normalize-package-data@npm:^2.4.0":
|
||||
version: 2.4.1
|
||||
resolution: "@types/normalize-package-data@npm:2.4.1"
|
||||
@@ -12329,7 +12360,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-visitor-keys@npm:^2.0.0":
|
||||
"eslint-visitor-keys@npm:^2.0.0, eslint-visitor-keys@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "eslint-visitor-keys@npm:2.1.0"
|
||||
checksum: e3081d7dd2611a35f0388bbdc2f5da60b3a3c5b8b6e928daffff7391146b434d691577aa95064c8b7faad0b8a680266bcda0a42439c18c717b80e6718d7e267d
|
||||
@@ -25896,7 +25927,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^4.4.3, typescript@npm:^4.5.4":
|
||||
"typescript@npm:^4.5.4":
|
||||
version: 4.6.4
|
||||
resolution: "typescript@npm:4.6.4"
|
||||
bin:
|
||||
@@ -25906,27 +25937,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^4.4.4":
|
||||
version: 4.7.3
|
||||
resolution: "typescript@npm:4.7.3"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: fd13a1ce53790a36bb8350e1f5e5e384b5f6cb9b0635114a6d01d49cb99916abdcfbc13c7521cdae2f2d3f6d8bc4a8ae7625edf645a04ee940588cd5e7597b2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:~4.1.5":
|
||||
version: 4.1.6
|
||||
resolution: "typescript@npm:4.1.6"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 54aed909f94b16178c8a8d8911871b4e1c04454a3e6c82166715e28083e7ce6271e4d1df6f82c89544a4759b07aec780785032534e9c93b254e2107a18712c05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@^4.4.3#~builtin<compat/typescript>, typescript@patch:typescript@^4.5.4#~builtin<compat/typescript>":
|
||||
"typescript@patch:typescript@npm%3A^4.5.4#~builtin<compat/typescript>":
|
||||
version: 4.6.4
|
||||
resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin<compat/typescript>::version=4.6.4&hash=bda367"
|
||||
bin:
|
||||
@@ -25936,26 +25947,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@^4.4.4#~builtin<compat/typescript>":
|
||||
version: 4.7.3
|
||||
resolution: "typescript@patch:typescript@npm%3A4.7.3#~builtin<compat/typescript>::version=4.7.3&hash=bda367"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 8257ce7ecbbf9416da60045a76a99d473698ca9e973fa0ddab7137cacb1587255431176cbbcc801a650938c4dc8109ab88355774829a714fabe56a53a2fe4524
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@~4.1.5#~builtin<compat/typescript>":
|
||||
version: 4.1.6
|
||||
resolution: "typescript@patch:typescript@npm%3A4.1.6#~builtin<compat/typescript>::version=4.1.6&hash=bda367"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 3bd9915f236817e4e2d32dd0d90e8902875929f014bb87a478000e32adda91d12f0425931ee6f9d6a2bc7d0c9242588fcee1050ac294497dfabf27d3d73b335c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typical@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "typical@npm:4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user