467 lines
15 KiB
JavaScript
467 lines
15 KiB
JavaScript
import * as THREE from 'three'
|
|
import SelectionHelper from './SelectionHelper'
|
|
|
|
export default class InteractionHandler {
|
|
constructor(viewer) {
|
|
this.viewer = viewer
|
|
this.preventSelection = false
|
|
|
|
this.selectionHelper = new SelectionHelper(this.viewer, {
|
|
sectionBox: this.sectionBox,
|
|
hover: false
|
|
})
|
|
this.selectionMeshMaterial = new THREE.MeshLambertMaterial({
|
|
color: 0x0b55d2,
|
|
side: THREE.DoubleSide,
|
|
wireframe: false,
|
|
transparent: true,
|
|
opacity: 0.3
|
|
})
|
|
this.selectionMeshMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
|
// Fix overlapping faces flickering
|
|
this.selectionMeshMaterial.polygonOffset = true
|
|
this.selectionMeshMaterial.polygonOffsetFactor = -0.1
|
|
|
|
this.selectionLineMaterial = new THREE.LineBasicMaterial({ color: 0x0b55d2 })
|
|
this.selectionLineMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
|
|
|
this.selectionEdgesMaterial = new THREE.LineBasicMaterial({ color: 0x23f3bd })
|
|
this.selectionEdgesMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
|
|
|
this.selectedObjects = new THREE.Group()
|
|
this.viewer.scene.add(this.selectedObjects)
|
|
this.selectedObjects.renderOrder = 1000
|
|
this.selectionBox = new THREE.Group()
|
|
this.viewer.scene.add(this.selectionBox)
|
|
|
|
this.overlayMeshMaterial = new THREE.MeshLambertMaterial({
|
|
color: 0x57f7ff,
|
|
side: THREE.DoubleSide,
|
|
wireframe: false,
|
|
transparent: true,
|
|
opacity: 0.7
|
|
})
|
|
this.overlayMeshMaterial.clippingPlanes = this.viewer.sectionBox.planes
|
|
this.overlaidObjects = new THREE.Group()
|
|
this.viewer.scene.add(this.overlaidObjects)
|
|
this.overlaidObjects.renderOrder = 2000
|
|
|
|
this.selectedObjectsUserData = []
|
|
this.selectedRawObjects = []
|
|
|
|
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) {
|
|
this.deselectObjects()
|
|
}
|
|
})
|
|
}
|
|
|
|
// used to display objects selected by other users
|
|
overlayObjects(ids = []) {
|
|
this.overlaidObjects.clear()
|
|
|
|
const all = this.viewer.sceneManager.allObjects
|
|
const subsetToAdd = all.filter((obj) => ids.indexOf(obj.uuid) !== -1)
|
|
|
|
for (const obj of subsetToAdd) {
|
|
const selType = obj.type
|
|
switch (selType) {
|
|
case 'Group': {
|
|
const blockObjs = this.getBlockObjectsCloned(obj)
|
|
for (const child of blockObjs) {
|
|
child.material = this.overlayMeshMaterial
|
|
this.overlaidObjects.add(child)
|
|
}
|
|
break
|
|
}
|
|
case 'Mesh':
|
|
this.overlaidObjects.add(
|
|
new THREE.Mesh(obj.geometry, this.overlayMeshMaterial)
|
|
)
|
|
break
|
|
case 'Line':
|
|
this.overlaidObjects.add(
|
|
new THREE.Line(obj.geometry, this.overlayMeshMaterial)
|
|
)
|
|
break
|
|
}
|
|
}
|
|
this.viewer.needsRender = true
|
|
}
|
|
|
|
_handleDoubleClick(objs) {
|
|
if (!objs || objs.length === 0) {
|
|
if (this.viewer.sectionBox.display.visible) {
|
|
this.zoomToObject(this.viewer.sectionBox.cube)
|
|
} else {
|
|
this.zoomExtents()
|
|
}
|
|
} else this.zoomToObject(objs[0].object)
|
|
this.viewer.needsRender = true
|
|
this.viewer.emit(
|
|
'object-doubleclicked',
|
|
objs && objs.length !== 0 ? objs[0].object : null,
|
|
objs && objs.length !== 0 ? objs[0].point : null
|
|
)
|
|
}
|
|
|
|
_handleSelect(objs) {
|
|
if (this.viewer.cameraHandler.orbiting) return
|
|
if (this.preventSelection) return
|
|
|
|
if (objs.length === 0) {
|
|
this.deselectObjects()
|
|
return
|
|
}
|
|
|
|
if (!this.selectionHelper.multiSelect) this.deselectObjects()
|
|
|
|
let selType = objs[0].object.type
|
|
let rootBlock = null
|
|
if (
|
|
objs[0].object.parent?.userData?.speckle_type
|
|
?.toLowerCase()
|
|
.includes('blockinstance')
|
|
) {
|
|
selType = 'Block'
|
|
rootBlock = this.getParentBlock(objs[0].object.parent)
|
|
}
|
|
|
|
const objId =
|
|
selType === 'Block' ? rootBlock.userData.id : objs[0].object.userData.id
|
|
const objIdIndexCheck = this.selectedObjectsUserData.findIndex(
|
|
(o) => o.id === objId
|
|
)
|
|
if (objIdIndexCheck !== -1) {
|
|
if (this.selectionHelper.multiSelect) {
|
|
// TODO: deselect if in multiple selection mode
|
|
this.selectedObjectsUserData.splice(objIdIndexCheck, 1)
|
|
this.deselectObj(rootBlock ? rootBlock.userData.id : objs[0].object.userData.id)
|
|
}
|
|
return
|
|
}
|
|
|
|
switch (selType) {
|
|
case 'Block': {
|
|
const blockObjs = this.getBlockObjectsCloned(rootBlock)
|
|
for (const child of blockObjs) {
|
|
child.userData = { id: rootBlock.userData.id }
|
|
child.material = this.selectionMeshMaterial
|
|
this.selectedObjects.add(child)
|
|
//this.viewer.outlinePass.selectedObjects.push( child )
|
|
}
|
|
break
|
|
}
|
|
case 'Mesh': {
|
|
const m = new THREE.Mesh(objs[0].object.geometry, this.selectionMeshMaterial)
|
|
m.userData = { id: objs[0].object.userData.id }
|
|
this.selectedObjects.add(m)
|
|
//this.viewer.outlinePass.selectedObjects.push( new THREE.Mesh( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
|
break
|
|
}
|
|
case 'Line': {
|
|
const l = new THREE.Line(objs[0].object.geometry, this.selectionMeshMaterial)
|
|
l.userData = { id: objs[0].object.userData.id }
|
|
this.selectedObjects.add(l)
|
|
//this.viewer.outlinePass.selectedObjects.push( new THREE.Line( objs[0].object.geometry, this.selectionMeshMaterial ) )
|
|
break
|
|
}
|
|
case 'Point':
|
|
console.warn('Point selection not implemented.')
|
|
return // exit the whole func here, points cause all sorts of trouble when being selected (ie, bbox stuff)
|
|
}
|
|
|
|
if (selType === 'Block') {
|
|
this.selectedObjectsUserData.push(rootBlock.userData)
|
|
this.selectedRawObjects.push(rootBlock)
|
|
} else {
|
|
this.selectedObjectsUserData.push(objs[0].object.userData)
|
|
this.selectedRawObjects.push(objs[0])
|
|
}
|
|
|
|
const box = new THREE.Box3().setFromObject(this.selectedObjects)
|
|
const boxHelper = new THREE.Box3Helper(box, 0x047efb)
|
|
this.selectionBox.clear()
|
|
this.selectionBox.add(boxHelper)
|
|
this.viewer.needsRender = true
|
|
|
|
const selectionCenter = new THREE.Vector3()
|
|
box.getCenter(selectionCenter)
|
|
const selectionInfo = {
|
|
userData: this.selectedObjectsUserData,
|
|
location: objs[0].point,
|
|
selectionCenter
|
|
}
|
|
this.viewer.emit('select', selectionInfo)
|
|
}
|
|
|
|
getParentBlock(block) {
|
|
if (block.parent?.userData?.speckle_type?.toLowerCase().includes('blockinstance')) {
|
|
return this.getParentBlock(block.parent)
|
|
} else return block
|
|
}
|
|
|
|
getBlockObjectsCloned(block, objects = []) {
|
|
for (const child of block.children) {
|
|
if (child instanceof THREE.Group) {
|
|
objects.push(...this.getBlockObjectsCloned(child))
|
|
} else {
|
|
objects.push(child.clone())
|
|
}
|
|
}
|
|
for (const child of objects) {
|
|
child.geometry = child.geometry.clone().applyMatrix4(block.matrix)
|
|
}
|
|
return objects
|
|
}
|
|
|
|
deselectObj(id) {
|
|
const objToRemove = this.selectedObjects.children.filter(
|
|
(o) => o.userData.id === id
|
|
)
|
|
for (const o of objToRemove) this.selectedObjects.remove(o)
|
|
|
|
this.selectionBox.clear()
|
|
if (this.selectedObjects.children.length !== 0) {
|
|
const box = new THREE.Box3().setFromObject(this.selectedObjects)
|
|
const boxHelper = new THREE.Box3Helper(box, 0x047efb)
|
|
this.selectionBox.add(boxHelper)
|
|
}
|
|
this.viewer.needsRender = true
|
|
}
|
|
|
|
deselectObjects() {
|
|
this.selectedObjects.clear()
|
|
this.selectionBox.clear()
|
|
this.selectedObjectsUserData = []
|
|
this.selectedRawObjects = []
|
|
this.viewer.needsRender = true
|
|
this.viewer.emit('select', { userData: [], location: null })
|
|
}
|
|
|
|
zoomToObjectId(id) {
|
|
const obj = this.viewer.sceneManager.allObjects.find((o) => o.uuid === id)
|
|
if (obj) this.zoomToObject(obj)
|
|
else console.warn(`No object with id of ${id} found.`)
|
|
}
|
|
|
|
zoomToObject(target, fit = 1.2, transition = true) {
|
|
const box = new THREE.Box3().setFromObject(target)
|
|
this.zoomToBox(box, fit, transition)
|
|
}
|
|
|
|
zoomExtents(fit = 1.2, transition = true) {
|
|
if (this.viewer.sectionBox.display.visible) {
|
|
this.zoomToObject(this.viewer.sectionBox.cube)
|
|
return
|
|
}
|
|
if (this.viewer.sceneManager.sceneObjects.objectsInScene.length === 0) {
|
|
const box = new THREE.Box3(
|
|
new THREE.Vector3(-1, -1, -1),
|
|
new THREE.Vector3(1, 1, 1)
|
|
)
|
|
this.zoomToBox(box, fit, transition)
|
|
return
|
|
}
|
|
|
|
const box = new THREE.Box3().setFromObject(
|
|
this.viewer.sceneManager.sceneObjects.objectsInScene
|
|
)
|
|
this.zoomToBox(box, fit, transition)
|
|
// this.viewer.controls.setBoundary( box )
|
|
}
|
|
|
|
zoomToBox(box, fit = 1.2, transition = true) {
|
|
if (box.max.x === Infinity || box.max.x === -Infinity) {
|
|
box = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1))
|
|
}
|
|
const fitOffset = fit
|
|
|
|
const size = box.getSize(new THREE.Vector3())
|
|
const target = new THREE.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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
rotateCamera(azimuthAngle = 0.261799, polarAngle = 0, transition = true) {
|
|
this.viewer.cameraHandler.controls.rotate(azimuthAngle, polarAngle, transition)
|
|
}
|
|
|
|
screenshot() {
|
|
const sectionBoxVisible = this.viewer.sectionBox.display.visible
|
|
if (sectionBoxVisible) {
|
|
this.viewer.sectionBox.displayOff()
|
|
this.viewer.needsRender = true
|
|
this.viewer.render()
|
|
}
|
|
const screenshot = this.viewer.renderer.domElement.toDataURL('image/png')
|
|
if (sectionBoxVisible) {
|
|
this.viewer.sectionBox.displayOn()
|
|
}
|
|
return screenshot
|
|
}
|
|
|
|
/**
|
|
* 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]
|
|
*/
|
|
rotateTo(side, 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.viewer.sceneManager.sceneObjects.allObjects.children.length === 0)
|
|
box = new THREE.Box3(
|
|
new THREE.Vector3(-1, -1, -1),
|
|
new THREE.Vector3(1, 1, 1)
|
|
)
|
|
else
|
|
box = new THREE.Box3().setFromObject(
|
|
this.viewer.sceneManager.sceneObjects.allObjects
|
|
)
|
|
if (box.max.x === Infinity || box.max.x === -Infinity) {
|
|
box = new THREE.Box3(
|
|
new THREE.Vector3(-1, -1, -1),
|
|
new THREE.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
|
|
}
|
|
}
|
|
}
|
|
|
|
getViews() {
|
|
return this.viewer.sceneManager.views.map((v) => {
|
|
return { name: v.applicationId, id: v.id, view: v }
|
|
})
|
|
}
|
|
|
|
setView(id, transition = true) {
|
|
if (!id) return
|
|
const view = this.viewer.sceneManager.views.find((v) => v.id === id)
|
|
if (!view) {
|
|
console.warn(`View id ${id} not found.`)
|
|
return
|
|
}
|
|
|
|
const target = view.target
|
|
const position = view.origin
|
|
|
|
this.viewer.cameraHandler.activeCam.controls.setLookAt(
|
|
position.x,
|
|
position.y,
|
|
position.z,
|
|
target.x,
|
|
target.y,
|
|
target.z,
|
|
transition
|
|
)
|
|
}
|
|
|
|
setLookAt(position, target, transition = true) {
|
|
if (!position || !target) return
|
|
this.viewer.cameraHandler.activeCam.controls.setLookAt(
|
|
position.x,
|
|
position.y,
|
|
position.z,
|
|
target.x,
|
|
target.y,
|
|
target.z,
|
|
transition
|
|
)
|
|
}
|
|
}
|