diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index b9047a096..6af5887a9 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -10,6 +10,8 @@ export default class Sandbox { private tabs private filterControls private steamsFolder + private viewsControls = [] + private viewsFolder private streams: { [url: string]: Array } = {} public static urlParams = { @@ -47,6 +49,17 @@ export default class Sandbox { viewer.on('load-complete', (url: string) => { this.addStreamControls(url) + this.addViewControls() + }) + viewer.on('unload-complete', (url: string) => { + this.removeViewControls() + this.addViewControls() + url + }) + viewer.on('unload-all-complete', (url: string) => { + this.removeViewControls() + this.addViewControls() + url }) } @@ -89,6 +102,27 @@ export default class Sandbox { delete this.streams[url] } + private addViewControls() { + const views = this.viewer.getViews() + this.viewsFolder = this.tabs.pages[0].addFolder({ + title: 'Views', + expanded: true + }) + for (let k = 0; k < views.length; k++) { + this.viewsFolder + .addButton({ + title: views[k].name + }) + .on('click', () => { + this.viewer.setView(views[k].id) + }) + } + } + + private removeViewControls() { + this.viewsFolder.dispose() + } + public makeGenericUI() { this.tabs.pages[0].addInput(Sandbox.urlParams, 'url', { title: 'url' @@ -148,6 +182,28 @@ export default class Sandbox { showBatches.on('click', () => { this.viewer.speckleRenderer.debugShowBatches() }) + + const screenshot = this.tabs.pages[0].addButton({ + title: 'Screenshot' + }) + screenshot.on('click', async () => { + console.warn(await this.viewer.screenshot()) + }) + + const canonicalViewsFolder = this.tabs.pages[0].addFolder({ + title: 'Canonical Views', + expanded: true + }) + const sides = ['front', 'back', 'top', 'bottom', 'right', 'left', '3d'] + for (let k = 0; k < sides.length; k++) { + canonicalViewsFolder + .addButton({ + title: sides[k] + }) + .on('click', () => { + this.viewer.rotateTo(sides[k]) + }) + } } makeSceneUI() { diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 7069c5751..229dd45af 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -40,4 +40,6 @@ sandbox.makeGenericUI() sandbox.makeSceneUI() sandbox.makeFilteringUI() // Load demo object -sandbox.loadUrl('https://latest.speckle.dev/streams/ca0378725b/commits/e5a66562cc') +sandbox.loadUrl( + 'https://latest.speckle.dev/streams/92b620fb17/commits/0ffebbd2f6?c=%5B-30.45977,17.60411,23.71672,-2.20374,1.01037,3.58707,0,1%5D' +) diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index bf101b476..f24d50c94 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -47,12 +47,18 @@ export interface IViewer { sectionBoxOn(): void zoomExtents(fit?: number, transition?: boolean): void toggleCameraProjection(): void + getViews() + setView(id: string, transition: boolean) + // This shouldn't be part of the API, it should be handled through `setView` + rotateTo(side: string, transition: boolean) loadObject(url: string, token?: string, enableCaching?: boolean): Promise cancelLoad(url: string, unload?: boolean): Promise unloadObject(url: string): Promise unloadAll(): Promise + screenshot(): Promise + applyFilter(filter: unknown): Promise getObjectsProperties(includeAll?: boolean): unknown diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index ded070c6c..75727bec3 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -403,7 +403,7 @@ export default class SpeckleRenderer { } /** Taken from InteractionsHandler. Will revisit in the future */ - zoomToBox(box, fit = 1.2, transition = true) { + public zoomToBox(box, fit = 1.2, transition = true) { if (box.max.x === Infinity || box.max.x === -Infinity) { box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) } @@ -452,6 +452,91 @@ export default class SpeckleRenderer { } } + public setView(origin: Vector3, target: Vector3, transition = true) { + this.viewer.cameraHandler.activeCam.controls.setLookAt( + origin.x, + origin.y, + origin.z, + target.x, + target.y, + target.z, + transition + ) + } + + /** + * Rotates camera to some canonical views + * @param {string} side Can be any of front, back, up (top), down (bottom), right, left. + * @param {Number} fit [description] + * @param {Boolean} transition [description] + * @return {[type]} [description] + */ + public rotateTo(side: string, transition = true) { + const DEG90 = Math.PI * 0.5 + const DEG180 = Math.PI + + switch (side) { + case 'front': + this.viewer.cameraHandler.controls.rotateTo(0, DEG90, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case 'back': + this.viewer.cameraHandler.controls.rotateTo(DEG180, DEG90, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case 'up': + case 'top': + this.viewer.cameraHandler.controls.rotateTo(0, 0, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case 'down': + case 'bottom': + this.viewer.cameraHandler.controls.rotateTo(0, DEG180, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case 'right': + this.viewer.cameraHandler.controls.rotateTo(DEG90, DEG90, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case 'left': + this.viewer.cameraHandler.controls.rotateTo(-DEG90, DEG90, transition) + if (this.viewer.cameraHandler.activeCam.name === 'ortho') + this.viewer.cameraHandler.disableRotations() + break + + case '3d': + case '3D': + default: { + let box + if (this.allObjects.children.length === 0) + box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) + else box = new Box3().setFromObject(this.allObjects) + if (box.max.x === Infinity || box.max.x === -Infinity) { + box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) + } + this.viewer.cameraHandler.controls.setPosition( + box.max.x, + box.max.y, + box.max.z, + transition + ) + this.zoomExtents() + this.viewer.cameraHandler.enableRotations() + break + } + } + } + /** DEBUG */ public onObjectClickDebug(e) { const result: Intersection = this.intersections.intersect( diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index efa954732..38604919a 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -102,9 +102,6 @@ export class Viewer extends EventEmitter implements IViewer { WorldTree.getRenderTree(url).buildRenderTree() this.speckleRenderer.addRenderTree(url) this.zoomExtents() - const views = this.__getViews() - console.warn(`Found ${views.length} VIEWS:`) - console.warn(views) console.warn('Built stuff') }) } @@ -173,9 +170,45 @@ export class Viewer extends EventEmitter implements IViewer { this.cameraHandler.toggleCameras() } - public __getViews() { - return WorldTree.getInstance().findAll((node: TreeNode) => { - return node.model.renderView?.speckleType === SpeckleType.View3D + public getViews() { + 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 + } + }) + } + + public setView(id: string, transition: boolean): void { + const view3DNode = WorldTree.getInstance().findId(id) + this.speckleRenderer.setView( + view3DNode.model.raw.origin, + view3DNode.model.raw.target, + transition + ) + } + + public rotateTo(side: string, transition = true) { + this.speckleRenderer.rotateTo(side) + transition + } + + public screenshot(): Promise { + 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) }) } @@ -210,6 +243,7 @@ export class Viewer extends EventEmitter implements IViewer { if (--this.inProgressOperations === 0) { ;(this as EventEmitter).emit('busy', false) console.warn(`Removed subtree ${url}`) + ;(this as EventEmitter).emit('unload-complete', url) } } } @@ -231,6 +265,7 @@ export class Viewer extends EventEmitter implements IViewer { if (--this.inProgressOperations === 0) { ;(this as EventEmitter).emit('busy', false) console.warn(`Removed all subtrees`) + ;(this as EventEmitter).emit('unload-all-complete') } } }