436 lines
12 KiB
TypeScript
436 lines
12 KiB
TypeScript
import * as THREE from 'three'
|
|
import Stats from 'three/examples/jsm/libs/stats.module'
|
|
|
|
import ViewerObjectLoader from './ViewerObjectLoader'
|
|
import EventEmitter from './EventEmitter'
|
|
import CameraHandler from './context/CameraHanlder'
|
|
|
|
import SectionBox from './SectionBox'
|
|
import { Clock, Texture } from 'three'
|
|
import { Assets } from './Assets'
|
|
import { Optional } from '../helpers/typeHelper'
|
|
import {
|
|
CanonicalView,
|
|
DefaultViewerParams,
|
|
InlineView,
|
|
IViewer,
|
|
PolarView,
|
|
SpeckleView,
|
|
SunLightConfiguration,
|
|
ViewerEvent,
|
|
ViewerParams
|
|
} from '../IViewer'
|
|
import { World } from './World'
|
|
import { TreeNode, WorldTree } from './tree/WorldTree'
|
|
import SpeckleRenderer from './SpeckleRenderer'
|
|
import { FilteringManager, FilteringState } from './filtering/FilteringManager'
|
|
import { PropertyInfo, PropertyManager } from './filtering/PropertyManager'
|
|
import { SpeckleType } from './converter/GeometryConverter'
|
|
import { DataTree } from './tree/DataTree'
|
|
|
|
export class Viewer extends EventEmitter implements IViewer {
|
|
/** Container and optional stats element */
|
|
private container: HTMLElement
|
|
private stats: Optional<Stats>
|
|
|
|
/** Viewer params used at init time */
|
|
private startupParams: ViewerParams
|
|
|
|
/** Viewer components */
|
|
private static world: World = new World()
|
|
public static Assets: Assets
|
|
protected speckleRenderer: SpeckleRenderer
|
|
private filteringManager: FilteringManager
|
|
/** Legacy viewer components (will revisit soon) */
|
|
public sectionBox: SectionBox
|
|
public cameraHandler: CameraHandler
|
|
|
|
/** Render flag for on-demand rendering */
|
|
private _needsRender: boolean
|
|
|
|
/** Misc members */
|
|
private inProgressOperations: number
|
|
private clock: Clock
|
|
private loaders: { [id: string]: ViewerObjectLoader } = {}
|
|
|
|
public get needsRender(): boolean {
|
|
return this._needsRender
|
|
}
|
|
|
|
public set needsRender(value: boolean) {
|
|
this._needsRender = value || this._needsRender
|
|
}
|
|
|
|
/** Gets the World object. Currently it's used for info mostly */
|
|
public static get World(): World {
|
|
return this.world
|
|
}
|
|
|
|
public constructor(
|
|
container: HTMLElement,
|
|
params: ViewerParams = DefaultViewerParams
|
|
) {
|
|
super()
|
|
|
|
this.container = container || document.getElementById('renderer')
|
|
if (params.showStats) {
|
|
this.stats = Stats()
|
|
this.container.appendChild(this.stats.dom)
|
|
}
|
|
this.loaders = {}
|
|
this.startupParams = params
|
|
this.clock = new THREE.Clock()
|
|
this.inProgressOperations = 0
|
|
|
|
this.speckleRenderer = new SpeckleRenderer(this)
|
|
this.speckleRenderer.create(this.container)
|
|
window.addEventListener('resize', this.resize.bind(this), false)
|
|
|
|
new Assets(this.speckleRenderer.renderer)
|
|
this.filteringManager = new FilteringManager(this.speckleRenderer)
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
;(window as any)._V = this // For debugging!
|
|
|
|
this.cameraHandler = new CameraHandler(this)
|
|
this.sectionBox = new SectionBox(this)
|
|
this.sectionBox.off()
|
|
this.sectionBox.controls.addEventListener('change', () => {
|
|
this.speckleRenderer.updateClippingPlanes(this.sectionBox.planes)
|
|
})
|
|
|
|
this.frame()
|
|
this.resize()
|
|
this.needsRender = true
|
|
|
|
this.on(ViewerEvent.LoadComplete, (url) => {
|
|
WorldTree.getRenderTree(url).buildRenderTree()
|
|
this.speckleRenderer.addRenderTree(url)
|
|
this.zoom()
|
|
})
|
|
}
|
|
public setSectionBox(
|
|
box?: {
|
|
min: {
|
|
x: number
|
|
y: number
|
|
z: number
|
|
}
|
|
max: { x: number; y: number; z: number }
|
|
},
|
|
offset?: number
|
|
) {
|
|
if (!box) {
|
|
box = this.speckleRenderer.sceneBox
|
|
}
|
|
this.sectionBox.setBox(box, offset)
|
|
}
|
|
public setSectionBoxFromObjects(objectIds: string[], offset?: number) {
|
|
this.setSectionBox(this.speckleRenderer.boxFromObjects(objectIds), offset)
|
|
}
|
|
|
|
public getCurrentSectionBox() {
|
|
return this.sectionBox.getCurrentBox()
|
|
}
|
|
|
|
public resize() {
|
|
this.speckleRenderer.renderer.setSize(
|
|
this.container.offsetWidth,
|
|
this.container.offsetHeight
|
|
)
|
|
this.needsRender = true
|
|
}
|
|
|
|
private frame() {
|
|
this.update()
|
|
this.render()
|
|
}
|
|
|
|
private update() {
|
|
const delta = this.clock.getDelta()
|
|
this.needsRender = this.cameraHandler.controls.update(delta)
|
|
this.speckleRenderer.update(delta)
|
|
this.stats?.update()
|
|
requestAnimationFrame(this.frame.bind(this))
|
|
}
|
|
|
|
private render() {
|
|
if (this.needsRender) {
|
|
this.speckleRenderer.render(this.cameraHandler.activeCam.camera)
|
|
this._needsRender = false
|
|
}
|
|
}
|
|
|
|
public async init(): Promise<void> {
|
|
if (this.startupParams.environmentSrc) {
|
|
Assets.getEnvironment(this.startupParams.environmentSrc)
|
|
.then((value: Texture) => {
|
|
this.speckleRenderer.indirectIBL = value
|
|
})
|
|
.catch((reason) => {
|
|
console.warn(reason)
|
|
console.warn('Fallback to null environment!')
|
|
})
|
|
}
|
|
}
|
|
|
|
public on(eventType: ViewerEvent, listener: (arg) => void): void {
|
|
super.on(eventType, listener)
|
|
}
|
|
|
|
public getObjectProperties(
|
|
resourceURL: string = null,
|
|
bypassCache = true
|
|
): PropertyInfo[] {
|
|
return PropertyManager.getProperties(resourceURL, bypassCache)
|
|
}
|
|
|
|
public selectObjects(objectIds: string[]): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.selectObjects(objectIds))
|
|
})
|
|
}
|
|
|
|
public resetSelection(): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.resetSelection())
|
|
})
|
|
}
|
|
|
|
public hideObjects(
|
|
objectIds: string[],
|
|
stateKey: string = null,
|
|
includeDescendants = false,
|
|
ghost = false
|
|
): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(
|
|
this.filteringManager.hideObjects(
|
|
objectIds,
|
|
stateKey,
|
|
includeDescendants,
|
|
ghost
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
public showObjects(
|
|
objectIds: string[],
|
|
stateKey: string = null,
|
|
includeDescendants = false
|
|
): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(
|
|
this.filteringManager.showObjects(objectIds, stateKey, includeDescendants)
|
|
)
|
|
})
|
|
}
|
|
|
|
public isolateObjects(
|
|
objectIds: string[],
|
|
stateKey: string = null,
|
|
includeDescendants = false,
|
|
ghost = true
|
|
): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(
|
|
this.filteringManager.isolateObjects(
|
|
objectIds,
|
|
stateKey,
|
|
includeDescendants,
|
|
ghost
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
public unIsolateObjects(
|
|
objectIds: string[],
|
|
stateKey: string = null,
|
|
includeDescendants = false
|
|
): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(
|
|
this.filteringManager.unIsolateObjects(objectIds, stateKey, includeDescendants)
|
|
)
|
|
})
|
|
}
|
|
|
|
public highlightObjects(objectIds: string[], ghost = false): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.highlightObjects(objectIds, ghost))
|
|
})
|
|
}
|
|
|
|
public resetHighlight(): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.resetHighlight())
|
|
})
|
|
}
|
|
|
|
public setColorFilter(property: PropertyInfo, ghost = true): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.setColorFilter(property, ghost))
|
|
})
|
|
}
|
|
|
|
public removeColorFilter(): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.removeColorFilter())
|
|
})
|
|
}
|
|
|
|
public resetFilters(): Promise<FilteringState> {
|
|
return new Promise<FilteringState>((resolve) => {
|
|
resolve(this.filteringManager.reset())
|
|
})
|
|
}
|
|
|
|
/**
|
|
* LEGACY: Handles (or tries to handle) old viewer filtering.
|
|
* @param args legacy filter object
|
|
*/
|
|
public async applyFilter(filter: unknown) {
|
|
filter
|
|
// return this.FilteringManager.handleLegacyFilter(filter)
|
|
}
|
|
|
|
public getDataTree(): DataTree {
|
|
return WorldTree.getDataTree()
|
|
}
|
|
|
|
public toggleSectionBox() {
|
|
this.sectionBox.toggle()
|
|
}
|
|
|
|
public sectionBoxOff() {
|
|
this.sectionBox.off()
|
|
}
|
|
|
|
public sectionBoxOn() {
|
|
this.sectionBox.on()
|
|
}
|
|
|
|
public zoom(objectIds?: string[], fit?: number, transition?: boolean) {
|
|
this.speckleRenderer.zoom(objectIds, fit, transition)
|
|
}
|
|
|
|
public setProjectionMode(mode: typeof CameraHandler.prototype.activeCam) {
|
|
this.cameraHandler.activeCam = mode
|
|
}
|
|
|
|
public toggleCameraProjection() {
|
|
this.cameraHandler.toggleCameras()
|
|
}
|
|
|
|
public setLightConfiguration(config: SunLightConfiguration): void {
|
|
this.speckleRenderer.setSunLightConfiguration(config)
|
|
}
|
|
|
|
public getViews(): SpeckleView[] {
|
|
return WorldTree.getInstance()
|
|
.findAll((node: TreeNode) => {
|
|
return node.model.renderView?.speckleType === SpeckleType.View3D
|
|
})
|
|
.map((v) => {
|
|
return {
|
|
name: v.model.raw.applicationId,
|
|
id: v.model.id,
|
|
view: v.model.raw
|
|
} as SpeckleView
|
|
})
|
|
}
|
|
|
|
public setView(
|
|
view: CanonicalView | SpeckleView | InlineView | PolarView,
|
|
transition = true
|
|
): void {
|
|
this.speckleRenderer.setView(view, transition)
|
|
this.needsRender = true
|
|
}
|
|
|
|
public screenshot(): Promise<string> {
|
|
return new Promise((resolve) => {
|
|
const sectionBoxVisible = this.sectionBox.display.visible
|
|
if (sectionBoxVisible) {
|
|
this.sectionBox.displayOff()
|
|
}
|
|
const screenshot = this.speckleRenderer.renderer.domElement.toDataURL('image/png')
|
|
if (sectionBoxVisible) {
|
|
this.sectionBox.displayOn()
|
|
}
|
|
resolve(screenshot)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* OBJECT LOADING/UNLOADING
|
|
*/
|
|
public async loadObject(url: string, token: string = null, enableCaching = true) {
|
|
try {
|
|
if (++this.inProgressOperations === 1)
|
|
(this as EventEmitter).emit(ViewerEvent.Busy, true)
|
|
|
|
const loader = new ViewerObjectLoader(this, url, token, enableCaching)
|
|
this.loaders[url] = loader
|
|
await loader.load()
|
|
} finally {
|
|
if (--this.inProgressOperations === 0)
|
|
(this as EventEmitter).emit(ViewerEvent.Busy, false)
|
|
}
|
|
}
|
|
|
|
public async cancelLoad(url: string, unload = false) {
|
|
this.loaders[url].cancelLoad()
|
|
if (unload) {
|
|
await this.unloadObject(url)
|
|
}
|
|
return
|
|
}
|
|
|
|
public async unloadObject(url: string) {
|
|
try {
|
|
if (++this.inProgressOperations === 1)
|
|
(this as EventEmitter).emit(ViewerEvent.Busy, true)
|
|
delete this.loaders[url]
|
|
this.speckleRenderer.removeRenderTree(url)
|
|
WorldTree.getRenderTree(url).purge()
|
|
WorldTree.getInstance().purge(url)
|
|
} finally {
|
|
if (--this.inProgressOperations === 0) {
|
|
;(this as EventEmitter).emit(ViewerEvent.Busy, false)
|
|
console.warn(`Removed subtree ${url}`)
|
|
;(this as EventEmitter).emit(ViewerEvent.UnloadComplete, url)
|
|
}
|
|
}
|
|
}
|
|
|
|
public async unloadAll() {
|
|
try {
|
|
if (++this.inProgressOperations === 1)
|
|
(this as EventEmitter).emit(ViewerEvent.Busy, true)
|
|
for (const key of Object.keys(this.loaders)) {
|
|
delete this.loaders[key]
|
|
}
|
|
this.filteringManager.reset()
|
|
WorldTree.getInstance().root.children.forEach((node) => {
|
|
this.speckleRenderer.removeRenderTree(node.model.id)
|
|
WorldTree.getRenderTree().purge()
|
|
})
|
|
|
|
WorldTree.getInstance().purge()
|
|
} finally {
|
|
if (--this.inProgressOperations === 0) {
|
|
;(this as EventEmitter).emit(ViewerEvent.Busy, false)
|
|
console.warn(`Removed all subtrees`)
|
|
;(this as EventEmitter).emit(ViewerEvent.UnloadAllComplete)
|
|
}
|
|
}
|
|
}
|
|
|
|
public dispose() {
|
|
// TODO: currently it's easier to simply refresh the page :)
|
|
}
|
|
}
|