From f73384e66f1eccfd89fc55b7bdbd0deef034fb06 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 21 Jun 2022 15:47:24 +0300 Subject: [PATCH] #811 Integrated double-click to zoom, and zoom extents with the new viewer. Currently I'm using old existing code just for the sake of less regression, but will revisit it in the future --- .../viewer/src/modules/InteractionHandler.js | 4 +- packages/viewer/src/modules/Intersections.ts | 57 +++---- .../viewer/src/modules/SpeckleRenderer.ts | 142 +++++++++++++++++- packages/viewer/src/modules/Viewer.ts | 4 +- packages/viewer/src/modules/input/Input.ts | 36 +++-- .../viewer/src/modules/legacy/SceneObjects.js | 4 +- .../src/modules/legacy/SelectionHelper.js | 2 +- .../viewer/src/modules/tree/NodeRenderView.ts | 11 ++ .../viewer/src/modules/tree/RenderTree.ts | 2 + 9 files changed, 198 insertions(+), 64 deletions(-) diff --git a/packages/viewer/src/modules/InteractionHandler.js b/packages/viewer/src/modules/InteractionHandler.js index c6d1af384..0f5a688fe 100644 --- a/packages/viewer/src/modules/InteractionHandler.js +++ b/packages/viewer/src/modules/InteractionHandler.js @@ -74,8 +74,8 @@ export default class InteractionHandler { this.selectedObjectsUserData = [] this.selectedRawObjects = [] - this.selectionHelper.on('object-doubleclicked', this._handleDoubleClick.bind(this)) - this.selectionHelper.on('object-clicked', this._handleSelect.bind(this)) + // this.selectionHelper.on('object-doubleclicked', this._handleDoubleClick.bind(this)) + // this.selectionHelper.on('object-clicked', this._handleSelect.bind(this)) document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.viewer.mouseOverRenderer) { diff --git a/packages/viewer/src/modules/Intersections.ts b/packages/viewer/src/modules/Intersections.ts index 57c1cce9c..4ef9044bf 100644 --- a/packages/viewer/src/modules/Intersections.ts +++ b/packages/viewer/src/modules/Intersections.ts @@ -1,46 +1,27 @@ -import { Raycaster, Scene } from 'three' -import Batcher from './batching/Batcher' -import { WorldTree } from './tree/WorldTree' +import { Camera, Intersection, Raycaster, Scene, Vector2 } from 'three' export class Intersections { - private scene: Scene - private batcher: Batcher - private lastTimeCall = 0 + private raycaster: Raycaster - public constructor(scene: Scene, batcher: Batcher) { - this.scene = scene - this.batcher = batcher - this.batcher + public constructor() { + this.raycaster = new Raycaster() + this.raycaster.params.Line = { threshold: 0.1 } + ;(this.raycaster.params as { Line2? }).Line2 = {} + ;(this.raycaster.params as { Line2? }).Line2.threshold = 1 } - public intersectScene(raycaster: Raycaster) { - if (performance.now() - this.lastTimeCall < 100) return - this.lastTimeCall = performance.now() - const target = this.scene.getObjectByName('ContentGroup') - const results = raycaster.intersectObjects(target.children) - if (!results.length) { - this.batcher.resetBatchesDrawRanges() - return - } + public intersect( + scene: Scene, + camera: Camera, + point: Vector2, + nearest = true + ): Intersection { + this.raycaster.setFromCamera(point, camera) + const target = scene.getObjectByName('ContentGroup') + const results = this.raycaster.intersectObjects(target.children) - results.sort((value) => value.distance) - console.warn(results[0]) - const rv = this.batcher.getRenderView( - results[0].object.uuid, - results[0].faceIndex !== undefined ? results[0].faceIndex : results[0].index - ) - const hitId = rv.renderData.id - - const hitNode = WorldTree.getInstance().findId(hitId) - console.warn(hitNode) - const renderViews = WorldTree.getRenderTree().getRenderViewsForNode(hitNode) - console.warn(renderViews) - this.batcher.selectRenderViews(renderViews) - // this.batcher.selectRenderView(rv) - // this.batcher.isolateRenderViews(renderViews) - - // const node1 = WorldTree.getInstance().findId('62942139c0010e51500ee10655ce33a6') - // const node2 = WorldTree.getInstance().findId('c6268101e02c2c5b1b3af25aed1f722b') - // this.batcher.isolateRenderViews([node1.model.renderView, node2.model.renderView]) + if (results.length === 0) return null + if (nearest) results.sort((value) => value.distance) + return results[0] } } diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index 39721b56a..226012ecd 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -1,24 +1,33 @@ import { AmbientLight, + Box3, Group, HemisphereLight, + Intersection, LinearToneMapping, PointLight, Scene, + Sphere, sRGBEncoding, Texture, + Vector3, WebGLRenderer } from 'three' import { GeometryType } from './batching/Batch' import Batcher from './batching/Batcher' import { SpeckleType } from './converter/GeometryConverter' +import Input, { InputOptionsDefault } from './input/Input' import { Intersections } from './Intersections' +import { WorldTree } from './tree/WorldTree' +import { Viewer } from './Viewer' export default class SceneManager { private _renderer: WebGLRenderer public scene: Scene private batcher: Batcher private intersections: Intersections + private input: Input + public viewer: Viewer // TEMPORARY public get renderer(): WebGLRenderer { return this._renderer @@ -28,11 +37,11 @@ export default class SceneManager { this.scene.environment = texture } - public constructor() { + public constructor(viewer: Viewer /** TEMPORARY */) { this.scene = new Scene() this.batcher = new Batcher() - this.intersections = new Intersections(this.scene, this.batcher) - this.intersections + this.intersections = new Intersections() + this.viewer = viewer } public create(container: HTMLElement) { @@ -49,6 +58,10 @@ export default class SceneManager { this._renderer.setSize(container.offsetWidth, container.offsetHeight) container.appendChild(this._renderer.domElement) + this.input = new Input(this._renderer.domElement, InputOptionsDefault) + this.input.on('object-clicked', this.onObjectClick.bind(this)) + this.input.on('object-doubleclicked', this.onObjectDoubleClick.bind(this)) + this.addDirectLights() } @@ -103,4 +116,127 @@ export default class SceneManager { hemiLight.up.set(0, 0, 1) this.scene.add(hemiLight) } + + private onObjectClick(e) { + const result: Intersection = this.intersections.intersect( + this.scene, + this.viewer.cameraHandler.activeCam.camera, + e + ) + if (!result) { + this.batcher.resetBatchesDrawRanges() + return + } + + // console.warn(result) + const rv = this.batcher.getRenderView( + result.object.uuid, + result.faceIndex !== undefined ? result.faceIndex : result.index + ) + const hitId = rv.renderData.id + + const hitNode = WorldTree.getInstance().findId(hitId) + // console.warn(hitNode) + const renderViews = WorldTree.getRenderTree().getRenderViewsForNode(hitNode) + // console.warn(renderViews) + this.batcher.selectRenderViews(renderViews) + // this.batcher.selectRenderView(rv) + // this.batcher.isolateRenderViews(renderViews) + } + + private onObjectDoubleClick(e) { + const result: Intersection = this.intersections.intersect( + this.scene, + this.viewer.cameraHandler.activeCam.camera, + e + ) + let rv = null + if (!result) { + if (this.viewer.sectionBox.display.visible) { + this.zoomToBox(this.viewer.sectionBox.cube, 1.2, true) + } else { + this.zoomExtents() + } + } else { + rv = this.batcher.getRenderView( + result.object.uuid, + result.faceIndex !== undefined ? result.faceIndex : result.index + ) + this.zoomToBox(rv.aabb, 1.2, true) + } + + this.viewer.needsRender = true + this.viewer.emit( + 'object-doubleclicked', + result ? rv.renderData.id : null, + result ? result.point : null + ) + } + + /** Taken from InteractionsHandler. Will revisit in the future */ + zoomExtents(fit = 1.2, transition = true) { + if (this.viewer.sectionBox.display.visible) { + this.zoomToBox(this.viewer.sectionBox.cube, 1.2, true) + return + } + if (this.scene.getObjectByName('ContentGroup').children.length === 0) { + const box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) + this.zoomToBox(box, fit, transition) + return + } + + const box = new Box3().setFromObject(this.scene.getObjectByName('ContentGroup')) + this.zoomToBox(box, fit, transition) + // this.viewer.controls.setBoundary( box ) + } + + /** Taken from InteractionsHandler. Will revisit in the future */ + 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)) + } + const fitOffset = fit + + const size = box.getSize(new Vector3()) + const target = new Sphere() + box.getBoundingSphere(target) + target.radius = target.radius * fitOffset + + const maxSize = Math.max(size.x, size.y, size.z) + const camFov = this.viewer.cameraHandler.camera.fov + ? this.viewer.cameraHandler.camera.fov + : 55 + const camAspect = this.viewer.cameraHandler.camera.aspect + ? this.viewer.cameraHandler.camera.aspect + : 1.2 + const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camFov) / 360)) + const fitWidthDistance = fitHeightDistance / camAspect + const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + + this.viewer.cameraHandler.controls.fitToSphere(target, transition) + + this.viewer.cameraHandler.controls.minDistance = distance / 100 + this.viewer.cameraHandler.controls.maxDistance = distance * 100 + this.viewer.cameraHandler.camera.near = distance / 100 + this.viewer.cameraHandler.camera.far = distance * 100 + this.viewer.cameraHandler.camera.updateProjectionMatrix() + + if (this.viewer.cameraHandler.activeCam.name === 'ortho') { + this.viewer.cameraHandler.orthoCamera.far = distance * 100 + this.viewer.cameraHandler.orthoCamera.updateProjectionMatrix() + + // fit the camera inside, so we don't have clipping plane issues. + // WIP implementation + const camPos = this.viewer.cameraHandler.orthoCamera.position + let dist = target.distanceToPoint(camPos) + if (dist < 0) { + dist *= -1 + this.viewer.cameraHandler.controls.setPosition( + camPos.x + dist, + camPos.y + dist, + camPos.z + dist + ) + } + } + } } diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index c92230a67..6272dd5d4 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -23,7 +23,7 @@ export class Viewer extends EventEmitter implements IViewer { private container: HTMLElement private stats: Optional private loaders: { [id: string]: ViewerObjectLoader } = {} - private needsRender: boolean + public needsRender: boolean private inProgressOperations: number public sectionBox: SectionBox @@ -87,7 +87,7 @@ export class Viewer extends EventEmitter implements IViewer { this.container = container || document.getElementById('renderer') - this.speckleRenderer = new SpeckleRenderer() + this.speckleRenderer = new SpeckleRenderer(this) this.speckleRenderer.create(this.container) Viewer.Assets = new Assets(this.speckleRenderer.renderer) diff --git a/packages/viewer/src/modules/input/Input.ts b/packages/viewer/src/modules/input/Input.ts index 6113edefc..e394db6cd 100644 --- a/packages/viewer/src/modules/input/Input.ts +++ b/packages/viewer/src/modules/input/Input.ts @@ -1,15 +1,18 @@ +import { Vector2 } from 'three' import EventEmitter from '../EventEmitter' export interface InputOptions { hover: boolean } -export default class SelectionHelper extends EventEmitter { - private pointerDown = false +export const InputOptionsDefault = { + hover: false +} + +export default class Input extends EventEmitter { private tapTimeout private lastTap = 0 private touchLocation: Touch - private multiSelect = false private container constructor(container: HTMLElement, _options: InputOptions) { @@ -27,7 +30,6 @@ export default class SelectionHelper extends EventEmitter { this.container.addEventListener('pointerup', (e) => { e.preventDefault() const delta = new Date().getTime() - mdTime - this.pointerDown = false if (delta > 250) return @@ -65,15 +67,15 @@ export default class SelectionHelper extends EventEmitter { }) // Handle multiple object selection - document.addEventListener('keydown', (e) => { - if (e.isComposing || e.keyCode === 229) return - if (e.key === 'Shift') this.multiSelect = true - }) + // document.addEventListener('keydown', (e) => { + // if (e.isComposing || e.keyCode === 229) return + // if (e.key === 'Shift') this.multiSelect = true + // }) - document.addEventListener('keyup', (e) => { - if (e.isComposing || e.keyCode === 229) return - if (e.key === 'Shift') this.multiSelect = false - }) + // document.addEventListener('keyup', (e) => { + // if (e.isComposing || e.keyCode === 229) return + // if (e.key === 'Shift') this.multiSelect = false + // }) } _getNormalisedClickPosition(e) { @@ -85,10 +87,12 @@ export default class SelectionHelper extends EventEmitter { x: ((e.clientX - rect.left) * canvas.width) / rect.width, y: ((e.clientY - rect.top) * canvas.height) / rect.height } - return { - x: (pos.x / canvas.width) * 2 - 1, - y: (pos.y / canvas.height) * -2 + 1 - } + const v = new Vector2( + (pos.x / canvas.width) * 2 - 1, + (pos.y / canvas.height) * -2 + 1 + ) + console.warn(v) + return v } dispose() { diff --git a/packages/viewer/src/modules/legacy/SceneObjects.js b/packages/viewer/src/modules/legacy/SceneObjects.js index 17268298e..94b5d32e7 100644 --- a/packages/viewer/src/modules/legacy/SceneObjects.js +++ b/packages/viewer/src/modules/legacy/SceneObjects.js @@ -1,7 +1,7 @@ import * as THREE from 'three' import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils' -import { Geometry } from './converter/Geometry' -import FilteringManager from './FilteringManager' +import { Geometry } from '../converter/Geometry' +import FilteringManager from '../FilteringManager' /** * Container for the scene objects, to allow loading/unloading/filtering/coloring/grouping diff --git a/packages/viewer/src/modules/legacy/SelectionHelper.js b/packages/viewer/src/modules/legacy/SelectionHelper.js index dda1b56ac..22c467068 100644 --- a/packages/viewer/src/modules/legacy/SelectionHelper.js +++ b/packages/viewer/src/modules/legacy/SelectionHelper.js @@ -129,7 +129,7 @@ export default class _SelectionHelper extends EventEmitter { normalizedPosition, this.viewer.cameraHandler.activeCam.camera ) - this.viewer.intersections.intersectScene(this.raycaster) + // this.viewer.intersections.intersectScene(this.raycaster) /** * This 'subset' thing is really weird and it's breaking picking. I would gladly * do something about it, however I'm afraid that it will open up a can of worms, diff --git a/packages/viewer/src/modules/tree/NodeRenderView.ts b/packages/viewer/src/modules/tree/NodeRenderView.ts index 9f4059a52..0f5c3c2ed 100644 --- a/packages/viewer/src/modules/tree/NodeRenderView.ts +++ b/packages/viewer/src/modules/tree/NodeRenderView.ts @@ -1,3 +1,4 @@ +import { Box3 } from 'three' import { GeometryType } from '../batching/Batch' import { GeometryData } from '../converter/Geometry' import { SpeckleType } from '../converter/GeometryConverter' @@ -31,6 +32,8 @@ export class NodeRenderView { private _materialHash: number private _geometryType: GeometryType + private _aabb: Box3 = null + public static readonly NullRenderMaterialHash = this.hashCode( GeometryType.MESH.toString() ) @@ -83,6 +86,10 @@ export class NodeRenderView { return this._batchId } + public get aabb() { + return this._aabb + } + public get needsSegmentConversion() { return ( this._renderData.speckleType === SpeckleType.Curve || @@ -110,6 +117,10 @@ export class NodeRenderView { this._batchIndexCount = count } + public computeAABB() { + this._aabb = new Box3().setFromArray(this._renderData.geometry.attributes.POSITION) + } + public getGeometryType(): GeometryType { switch (this._renderData.speckleType) { case SpeckleType.Mesh: diff --git a/packages/viewer/src/modules/tree/RenderTree.ts b/packages/viewer/src/modules/tree/RenderTree.ts index 5485c2efe..44df61d56 100644 --- a/packages/viewer/src/modules/tree/RenderTree.ts +++ b/packages/viewer/src/modules/tree/RenderTree.ts @@ -22,7 +22,9 @@ export class RenderTree { transform.premultiply(rendeNode.geometry.bakeTransform) } Geometry.transformGeometryData(rendeNode.geometry, transform) + node.model.renderView.computeAABB() } + return true }) }