Clean up code, added comments, styled UI
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
<main class="relative">
|
||||
<SpeckleViewer />
|
||||
<ControlPanel />
|
||||
<SelectionPanel />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SpeckleViewer from './components/SpeckleViewer.vue'
|
||||
import ControlPanel from './components/ControlPanel.vue'
|
||||
import SelectionPanel from './components/SelectionPanel.vue'
|
||||
</script>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div class="absolute z-10 top-4 left-4 flex gap-x-2">
|
||||
<div
|
||||
class="bg-white rounded-xl overflow-hidden border border-outline-2 flex flex-col shadow-md min-w-72"
|
||||
>
|
||||
<div class="flex items-center py-3 px-4 border-b border-outline-2">
|
||||
<h2 class="text-sm font-medium text-gray-800">Control Panel</h2>
|
||||
</div>
|
||||
<div class="py-3 px-4">
|
||||
<button @click="doStuff">Get materials</button>
|
||||
<button @click="doStuff2">Get materials 2</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-screen w-screen">
|
||||
<div class="h-screen w-screen" ref="canvas" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef, ref } from 'vue'
|
||||
import useViewer from '@/composables/viewer'
|
||||
import {
|
||||
CameraController,
|
||||
DefaultViewerParams,
|
||||
SelectionExtension,
|
||||
FilteringExtension,
|
||||
SpeckleLoader,
|
||||
UrlHelper,
|
||||
Viewer,
|
||||
ViewerEvent,
|
||||
type SelectionEvent
|
||||
} from '@speckle/viewer'
|
||||
import { Catalogue } from './../extensions/Catalogue'
|
||||
|
||||
const canvas = useTemplateRef('canvas')
|
||||
// const { init: initViewer, loadModelFromUrl, viewer,getObjectProperties } = useViewer()
|
||||
|
||||
const material = ref(null)
|
||||
let viewer = null
|
||||
|
||||
onMounted(async () => {
|
||||
// if (!canvas.value) return
|
||||
// await initViewer(canvas.value)
|
||||
// await loadModelFromUrl(
|
||||
// 'https://app.speckle.systems/projects/24c98619ac/models/38639656b8',
|
||||
// )
|
||||
|
||||
// console.log(viewer.getObjectProperties())
|
||||
|
||||
// // await getObjectProperties()
|
||||
|
||||
const params = DefaultViewerParams
|
||||
params.showStats = false
|
||||
params.verbose = true
|
||||
|
||||
viewer = new Viewer(canvas.value, params)
|
||||
|
||||
await viewer.init()
|
||||
|
||||
viewer.createExtension(CameraController)
|
||||
viewer.createExtension(SelectionExtension)
|
||||
viewer.createExtension(Catalogue)
|
||||
viewer.createExtension(FilteringExtension)
|
||||
|
||||
const urls = await UrlHelper.getResourceUrls('https://app.speckle.systems/projects/24c98619ac/models/38639656b8')
|
||||
urls.forEach(async (url) => {
|
||||
if (!viewer) return
|
||||
const loader = new SpeckleLoader(viewer.getWorldTree(), url, '')
|
||||
await viewer.loadObject(loader, true)
|
||||
})
|
||||
|
||||
viewer.on(ViewerEvent.LoadComplete, async() => {
|
||||
const properties = await viewer.getObjectProperties()
|
||||
console.log('properties', properties)
|
||||
|
||||
material.value = properties.find(property => property.key === 'renderMaterial.name')
|
||||
console.log('material', material.value)
|
||||
})
|
||||
|
||||
|
||||
viewer.on(ViewerEvent.ObjectClicked, (event: SelectionEvent | null) => {
|
||||
if (event) {
|
||||
console.log(event.hits[0].node.model.id)
|
||||
|
||||
const nodes = viewer.getWorldTree().findId(event.hits[0].node.model.id)
|
||||
console.log('nodes', nodes[0])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const doStuff = () => {
|
||||
console.log('material', material.value)
|
||||
const catalogue = viewer.getExtension(Catalogue)
|
||||
catalogue.categorize(material.value.valueGroups)
|
||||
catalogue.animate()
|
||||
}
|
||||
|
||||
const doStuff2 = () => {
|
||||
console.log('material', material.value)
|
||||
|
||||
const filter = viewer.getExtension(FilteringExtension)
|
||||
filter.isolateObjects(material.value.valueGroups[0].ids)
|
||||
|
||||
|
||||
// catalogue.categorize(material.value.valueGroups)
|
||||
// catalogue.animate()
|
||||
}
|
||||
</script>
|
||||
@@ -1,25 +1,24 @@
|
||||
<template>
|
||||
<div class="absolute z-10 top-4 left-4 flex gap-x-2">
|
||||
<button
|
||||
@click="isOpen = !isOpen"
|
||||
class="transition rounded-lg w-10 h-10 flex items-center justify-center shadow-md bg-white outline-none border"
|
||||
:class="{ 'bg-gray-100': isOpen }"
|
||||
>
|
||||
<IconCog />
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="bg-white rounded-xl overflow-hidden border border-outline-2 flex flex-col shadow-md min-w-72"
|
||||
>
|
||||
<div class="flex items-center py-3 px-4 border-b border-outline-2">
|
||||
<h2 class="text-sm font-medium text-gray-800">Control Panel</h2>
|
||||
<h2 class="text-sm font-medium text-gray-800">Levels</h2>
|
||||
</div>
|
||||
<div class="py-3 px-4">
|
||||
|
||||
<button @click="getProperties">Get materials</button>
|
||||
|
||||
<div v-for="(material, index) in materials?.valueGroups" :key="index">
|
||||
<button @click="categorize([material])">{{ material.value }}</button>
|
||||
<div class="flex gap-x-2">
|
||||
<BaseButton @click="getLevels">Get levels</BaseButton>
|
||||
<BaseButton @click="categorizeLevels">Categorize levels</BaseButton>
|
||||
<BaseButton @click="uncategorizeLevels">Uncategorize levels</BaseButton>
|
||||
</div>
|
||||
<div v-if="levels" class="flex flex-col gap-y-2 text-sm mt-4 mb-2">
|
||||
<div
|
||||
v-for="(property, index) in levels?.valueGroups"
|
||||
:key="`level-${index}`"
|
||||
>
|
||||
<button @click="isolate(property.ids)">{{ property.value }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,18 +26,33 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconCog from './icon/IconCog.vue'
|
||||
import { ref, toRaw } from 'vue'
|
||||
import useViewerActions from '@/composables/viewer/actions';
|
||||
import { ref } from 'vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import useViewer from '@/composables/viewer'
|
||||
import { properties } from '@/composables/viewer'
|
||||
import type { StringPropertyInfo, PropertyInfo } from '@speckle/viewer'
|
||||
|
||||
const { getObjectProperties, categorize } = useViewerActions()
|
||||
const { categorize, isolate, animate, resetFilters } = useViewer()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const materials = ref(null)
|
||||
const levels = ref()
|
||||
|
||||
const getProperties = async () => {
|
||||
const properties = await getObjectProperties()
|
||||
materials.value = properties.find(property => property.key === 'renderMaterial.name')
|
||||
console.log('properties', properties)
|
||||
// Get all the properties with the key 'properties.Instance Parameters.Constraints.Level.value'
|
||||
const getLevels = async () => {
|
||||
if (!properties) return
|
||||
levels.value = properties.find((property: PropertyInfo) => property.key === 'properties.Instance Parameters.Constraints.Level.value')
|
||||
}
|
||||
|
||||
// Categorize the levels and isolate the objects
|
||||
const categorizeLevels = async () => {
|
||||
await getLevels()
|
||||
categorize(levels.value.valueGroups)
|
||||
isolate(levels.value.valueGroups.flatMap((group: StringPropertyInfo['valueGroups']) => group.ids))
|
||||
animate()
|
||||
}
|
||||
|
||||
// Reset filters and reverse the animation
|
||||
const uncategorizeLevels = async () => {
|
||||
resetFilters()
|
||||
animate({ reverse: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="absolute z-10 top-4 right-4 flex gap-x-2">
|
||||
<div
|
||||
class="bg-white rounded-xl overflow-hidden border border-outline-2 flex flex-col shadow-md min-w-72"
|
||||
>
|
||||
<div class="flex items-center py-3 px-4 border-b border-outline-2">
|
||||
<h2 class="text-sm font-medium text-gray-800">Selection info</h2>
|
||||
</div>
|
||||
<div class="py-3 px-4 text-sm flex flex-col gap-y-1">
|
||||
<template v-if="selectionInfo">
|
||||
<div>
|
||||
<span class="font-medium">ID:</span> {{ selectionInfo.id }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Category:</span> {{ selectionInfo.category }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Level:</span> {{ selectionInfo.level.name }}
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="text-sm text-gray-500">No selection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useViewer from '@/composables/viewer'
|
||||
|
||||
const { selectionInfo } = useViewer()
|
||||
</script>
|
||||
@@ -6,16 +6,23 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue'
|
||||
import useViewer from '@/composables/viewer/viewer'
|
||||
import useViewer from '@/composables/viewer'
|
||||
|
||||
const canvas = useTemplateRef('canvas')
|
||||
const { init: initViewer, addExtensions,loadModelFromUrl } = useViewer()
|
||||
const { init, addExtensions,loadModelFromUrl } = useViewer()
|
||||
|
||||
// For demo purposes we will load two models
|
||||
// You can replace these with your own as well
|
||||
const MODELS = {
|
||||
ONE: 'https://app.speckle.systems/projects/7744b171ca/models/e32f5e5416',
|
||||
TWO: 'https://app.speckle.systems/projects/7744b171ca/models/7fee46df4b'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initViewer(canvas.value)
|
||||
addExtensions()
|
||||
await loadModelFromUrl(
|
||||
'https://app.speckle.systems/projects/24c98619ac/models/38639656b8'
|
||||
)
|
||||
if (!canvas.value) return
|
||||
|
||||
await init(canvas.value)
|
||||
addExtensions()
|
||||
await loadModelFromUrl(MODELS.ONE)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
class="w-5 h-5 text-gray-800"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<button class="bg-white rounded-md border border-outline-2 shadow text-sm py-1 px-2">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
CameraController,
|
||||
DefaultViewerParams,
|
||||
SelectionExtension,
|
||||
FilteringExtension,
|
||||
SpeckleLoader,
|
||||
UrlHelper,
|
||||
Viewer,
|
||||
ViewerEvent,
|
||||
type SelectionEvent,
|
||||
type PropertyInfo
|
||||
} from '@speckle/viewer'
|
||||
import { Catalogue, type CatalogueOptions } from '@/extensions/Catalogue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export let viewer: Viewer | undefined = undefined
|
||||
export let properties: PropertyInfo[] | undefined = undefined
|
||||
|
||||
const selectionInfo = ref(null)
|
||||
|
||||
export default function useViewer() {
|
||||
/**
|
||||
* Initialize the viewer
|
||||
* @param element - DOM element to initialize the viewer on
|
||||
*/
|
||||
async function init(element: HTMLDivElement) {
|
||||
const params = {
|
||||
...DefaultViewerParams,
|
||||
showStats: false,
|
||||
verbose: true
|
||||
}
|
||||
|
||||
viewer = new Viewer(element, params)
|
||||
await viewer.init()
|
||||
|
||||
// Get the object properties after the model has loaded
|
||||
// This will cache them and allow faster access later
|
||||
viewer.on(ViewerEvent.LoadComplete, async() => {
|
||||
properties = await viewer.getObjectProperties()
|
||||
})
|
||||
|
||||
// Handle object clicks in the viewer
|
||||
viewer.on(ViewerEvent.ObjectClicked, (event: SelectionEvent | null) => {
|
||||
// If there are no nodes, the click was not on an object
|
||||
if (event) {
|
||||
const nodes = viewer.getWorldTree().findId(event.hits[0].node.model.id)
|
||||
if (nodes && nodes.length > 0) {
|
||||
selectionInfo.value = nodes[0].model.raw
|
||||
}
|
||||
} else {
|
||||
selectionInfo.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The viewer can be extended with additional functionality by adding extensions
|
||||
* You can use extensions provided by the viewer, or create your own
|
||||
*/
|
||||
function addExtensions() {
|
||||
if (!viewer) return
|
||||
viewer.createExtension(CameraController)
|
||||
viewer.createExtension(SelectionExtension)
|
||||
viewer.createExtension(FilteringExtension)
|
||||
viewer.createExtension(Catalogue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a model from a Speckle URL
|
||||
* @param url - The URL of the Speckle model
|
||||
* @param authToken - This is required if the model is private
|
||||
*/
|
||||
const loadModelFromUrl = async (url: string, authToken?: string) => {
|
||||
const urls = await UrlHelper.getResourceUrls(url, authToken)
|
||||
urls.forEach(async (url) => {
|
||||
if (!viewer) return
|
||||
const loader = new SpeckleLoader(viewer.getWorldTree(), url, '')
|
||||
await viewer.loadObject(loader, true)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolate objects in the viewer
|
||||
* @param ids - List of IDs to isolate
|
||||
*/
|
||||
const isolate = async (ids: Array<string>) => {
|
||||
if (!viewer) return
|
||||
const filter = viewer.getExtension(FilteringExtension)
|
||||
filter.resetFilters()
|
||||
filter.isolateObjects(ids)
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a certain property in the viewer
|
||||
* @param input - ids to categorize
|
||||
* @param options - options for the catalogue
|
||||
*/
|
||||
const categorize = async (
|
||||
input: Array<{ ids: Array<string>; value: string }>,
|
||||
options?: CatalogueOptions
|
||||
) => {
|
||||
if (!viewer) return
|
||||
const catalogue = viewer.getExtension(Catalogue)
|
||||
catalogue.categorize(input, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the catalogue extension to animate the objects
|
||||
* @param options - option to reverse the animation
|
||||
*/
|
||||
const animate = async (options?:{ reverse?: boolean }) => {
|
||||
const { reverse } = options || {}
|
||||
if (!viewer) return
|
||||
const catalogue = viewer.getExtension(Catalogue)
|
||||
catalogue.animate(reverse)
|
||||
}
|
||||
|
||||
// Reset the filtering extension
|
||||
const resetFilters = async () => {
|
||||
if (!viewer) return
|
||||
const filter = viewer.getExtension(FilteringExtension)
|
||||
filter.resetFilters()
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
loadModelFromUrl,
|
||||
addExtensions,
|
||||
selectionInfo,
|
||||
isolate,
|
||||
categorize,
|
||||
animate,
|
||||
resetFilters
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import useViewer from '@/composables/viewer/viewer'
|
||||
import { Catalogue, type CatalogueOptions } from '@/extensions/Catalogue'
|
||||
import { FilteringExtension } from '@speckle/viewer'
|
||||
import { viewer } from '@/composables/viewer/viewer'
|
||||
|
||||
export default function useViewerActions() {
|
||||
/**
|
||||
* Get the properties of the objects in the viewer
|
||||
* The exact properties returned depends on the objects in the viewer
|
||||
* @returns - List of properties in the viewer
|
||||
*/
|
||||
const getObjectProperties = async () => {
|
||||
console.log(viewer)
|
||||
if (!viewer) return
|
||||
return await viewer.getObjectProperties()
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize and animate a certain property in the viewer
|
||||
* @param input - ids to categorize
|
||||
* @param options - options for the catalogue
|
||||
*/
|
||||
const categorize = async (
|
||||
input: Array<{ ids: Array<string>; value: string }>,
|
||||
options?: CatalogueOptions
|
||||
) => {
|
||||
if (!viewer) return
|
||||
const catalogue = viewer.getExtension(Catalogue)
|
||||
catalogue.categorize(input, options)
|
||||
catalogue.animate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolate objects in the viewer
|
||||
* @param ids - List of IDs to isolate
|
||||
*/
|
||||
const isolate = async (ids: Array<string>) => {
|
||||
if (!viewer) return
|
||||
const filter = viewer.getExtension(FilteringExtension)
|
||||
filter.isolateObjects(ids)
|
||||
}
|
||||
|
||||
return {
|
||||
getObjectProperties,
|
||||
categorize,
|
||||
isolate
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
CameraController,
|
||||
DefaultViewerParams,
|
||||
SelectionExtension,
|
||||
FilteringExtension,
|
||||
SpeckleLoader,
|
||||
UrlHelper,
|
||||
Viewer,
|
||||
ViewerEvent
|
||||
} from '@speckle/viewer'
|
||||
import { Catalogue } from '@/extensions/Catalogue'
|
||||
|
||||
export let viewer: Viewer | undefined = undefined
|
||||
|
||||
export default function useViewer() {
|
||||
/**
|
||||
* Initialize the viewer
|
||||
* @param element - DOM element to initialize the viewer on
|
||||
*/
|
||||
async function init(element: HTMLDivElement) {
|
||||
const params = {
|
||||
...DefaultViewerParams,
|
||||
showStats: false,
|
||||
verbose: true
|
||||
}
|
||||
|
||||
viewer = new Viewer(element, params)
|
||||
await viewer.init()
|
||||
|
||||
viewer.on(ViewerEvent.LoadComplete, async() => {
|
||||
const properties = await viewer.getObjectProperties()
|
||||
console.log('properties', properties)
|
||||
|
||||
// material.value = properties.find(property => property.key === 'renderMaterial.name')
|
||||
// console.log('material', material.value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The viewer can be extended with additional functionality by adding extensions
|
||||
* You can use extensions provided by the viewer, or create your own
|
||||
*/
|
||||
function addExtensions() {
|
||||
if (!viewer) return
|
||||
viewer.createExtension(CameraController)
|
||||
viewer.createExtension(SelectionExtension)
|
||||
viewer.createExtension(FilteringExtension)
|
||||
// Example of a custom extension
|
||||
viewer.createExtension(Catalogue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a model from a Speckle URL
|
||||
* @param url - The URL of the Speckle model
|
||||
* @param authToken - This is required if the model is private
|
||||
*/
|
||||
const loadModelFromUrl = async (url: string, authToken?: string) => {
|
||||
const urls = await UrlHelper.getResourceUrls(url, authToken)
|
||||
urls.forEach(async (url) => {
|
||||
if (!viewer) return
|
||||
const loader = new SpeckleLoader(viewer.getWorldTree(), url, '')
|
||||
await viewer.loadObject(loader, true)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
loadModelFromUrl,
|
||||
addExtensions
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user