UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

284 lines (222 loc) 9.95 kB
import {Event, EventDispatcher, Intersection, Raycaster, Vector2} from 'three' import {JSUndoManager, now} from 'ts-browser-helpers' import {ICamera, IObject3D} from '../../core' export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> { private _firstHit: IObject3D | undefined hoverEnabled = false /** * Time threshold for a pointer click event */ static PointerClickMaxTime = 200 /** * Distance threshold for a pointer click event */ static PointerClickMaxDistance = 0.1 // 1/20 of the canvas undoManager?: JSUndoManager private _root: IObject3D private _camera: ICamera private _mouseDownTime: number private _mouseDownPos: Vector2 = new Vector2() private _mouseUpTime: number private _time: number public selectionCondition: (o: IObject3D) => boolean public raycaster: Raycaster public mouse: Vector2 private _selected: IObject3D[] private _hovering: IObject3D[] public cursorStyles: {default: string; down: string} public domElement: HTMLElement constructor(root: IObject3D, domElement: HTMLElement, camera: ICamera, selectionCondition?: (o:IObject3D)=>boolean) { super() this._root = root this._camera = camera this.domElement = domElement this._time = this.time this._mouseDownTime = 0 this._mouseUpTime = 1 this.selectionCondition = selectionCondition ?? ( (selectedObject: any) => { return selectedObject.userData.userSelectable !== false && selectedObject.userData.bboxVisible !== false && selectedObject.material != null && selectedObject.material.type !== 'ShadowMaterial' // sample to select only mesh with material and not shadowmaterial. }) this.raycaster = new Raycaster() this.mouse = new Vector2() this._selected = [] this._hovering = [] this.cursorStyles = { default: 'grab', down: 'grabbing', } this.domElement.style.touchAction = 'none' // this.domElement.style.cursor = this.cursorStyles.default this.domElement.addEventListener('pointermove', this._onPointerMove) this.domElement.addEventListener('pointerleave', this._onPointerLeave) this.domElement.addEventListener('pointerout', this._onPointerLeave) this.domElement.addEventListener('pointercancel', this._onPointerCancel) this.domElement.addEventListener('pointerenter', this._onPointerEnter) this.domElement.addEventListener('pointerdown', this._onPointerDown) this.domElement.addEventListener('pointerup', this._onPointerUp) } dispose() { this.selectedObject = null this.hoverObject = null this.domElement.removeEventListener('pointermove', this._onPointerMove) this.domElement.removeEventListener('pointerleave', this._onPointerLeave) this.domElement.removeEventListener('pointerout', this._onPointerLeave) this.domElement.removeEventListener('pointercancel', this._onPointerCancel) this.domElement.removeEventListener('pointerenter', this._onPointerEnter) this.domElement.removeEventListener('pointerdown', this._onPointerDown) this.domElement.removeEventListener('pointerup', this._onPointerUp) } get camera() { return this._camera } set camera(value) { this._camera = value } get selectedObject(): IObject3D | null { return this._selected.length > 0 ? this._selected[0] : null } set selectedObject(object) { this._setSelected(object) } private _setSelected(object: IObject3D|null, record = true) { if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return const current = [...this._selected] this._selected = object ? Array.isArray(object) ? [...object] : [object] : [] this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject}) record && this.undoManager?.record({ undo: () => this._setSelected(current.length ? current[0] : null, false), redo: () => this._setSelected(object, false), }) } get hoverObject(): IObject3D | null { return this._hovering.length > 0 ? this._hovering[0] : null } set hoverObject(object: IObject3D | IObject3D[] | null) { if (!this._hovering.length && !object || this._hovering.length === 1 && this._hovering[0] === object) return this._hovering = object ? Array.isArray(object) ? [...object] : [object] : [] this.dispatchEvent({type: 'hoverObjectChanged', object: this.hoverObject}) } get time() { this._time = now() return this._time } get isMouseDown() { return this.mouseDownDeltaTime < 0 } get mouseDownDeltaTime() { return this._mouseUpTime - this._mouseDownTime } private _onPointerMove = (event: PointerEvent) => { if (event.isPrimary === false) return this.updateMouseFromEvent(event) if (this.hoverEnabled) this.hoverObject = this.checkIntersection()?.intersects[0].object ?? null } private _onPointerLeave = (event: PointerEvent) => { if (event.isPrimary === false) return this.domElement.style.cursor = this.cursorStyles.default // this.updateMouseFromEvent(event); if (this.hoverEnabled || this.hoverObject) this.hoverObject = null } private _onPointerEnter = (_: PointerEvent) => { // todo dispatch event? } private _onPointerCancel = (_: PointerEvent) => { // todo dispatch event? } updateMouseFromEvent(event: PointerEvent) { const rect = this.domElement.getBoundingClientRect() this.mouse.x = (event.clientX - rect.x) / rect.width * 2 - 1 this.mouse.y = -((event.clientY - rect.y) / rect.height) * 2 + 1 } private _onPointerDown = (event: PointerEvent) => { if (event.isPrimary === false) return this.domElement.style.cursor = this.cursorStyles.down this._mouseDownTime = this.time this._mouseDownPos.copy(this.mouse) return undefined } private _onPointerUp = (event: PointerEvent) => { if (event.isPrimary === false) return this.domElement.style.cursor = this.cursorStyles.default this._mouseUpTime = this.time const delta = this.mouseDownDeltaTime const dist = this._mouseDownPos.distanceTo(this.mouse) if (delta < ObjectPicker.PointerClickMaxTime && dist < ObjectPicker.PointerClickMaxDistance) { // click this._onPointerClick(event) } return undefined } private _onPointerClick = (event: PointerEvent) => { if (event.isPrimary === false) return this.updateMouseFromEvent(event) const intersects = this.checkIntersection() if (intersects) this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects}) else this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects: {selectedObject: null, intersect: null, intersects: []}}) this.selectedObject = intersects?.selectedObject || null } checkIntersection() { const camera = this._camera if (!camera) return null this.raycaster.setFromCamera(this.mouse, camera) let intersects = this.raycaster.intersectObject<IObject3D>(this._root, true) const uniqueIds: number[] = [] const uniqueIntersects = intersects.filter(element => { const isDuplicate = uniqueIds.includes(element.object.id) if (!isDuplicate) { uniqueIds.push(element.object.id) return true } return false }) intersects = uniqueIntersects let selectedObject:IObject3D | null = null let intersect: Intersection<IObject3D> | undefined const intersects2 = [] for (const intersect1 of intersects) { selectedObject = intersect1.object intersect = intersect1 while (selectedObject != null && (!selectedObject.visible || !this.selectionCondition(selectedObject))) { selectedObject = selectedObject.parent } if (selectedObject != null) intersects2.push(intersect1) } intersects = intersects2 if (intersects.length > 0) { selectedObject = intersects[0].object intersect = intersects[0] if (this._firstHit && selectedObject.id !== this._firstHit.id) { selectedObject = intersect.object } else { for (let i = 0; i < intersects.length; i++) { if (this.selectedObject && this.selectedObject.id === intersects[i].object.id) { const n = i + 1 // Use ( i + 1 ) % intersects.length for looping through objects if (n < intersects.length) { intersect = intersects[n] selectedObject = intersect.object } else { return null } } } } this._firstHit = intersects[0].object } if (selectedObject && intersect) { if (selectedObject) // sorted by distance return {selectedObject, intersect, intersects, mouse: this.mouse.toArray()} return null } else { return null } } isHovering() { return this.hoverObject != null // if something is highlighted. } isSelected() { return this.selectedObject != null // if something is selected. } }