UNPKG

threepipe

Version:

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

221 lines 9.4 kB
import { EventDispatcher, Raycaster, Vector2 } from 'three'; import { now } from 'ts-browser-helpers'; export class ObjectPicker extends EventDispatcher { constructor(root, domElement, camera, selectionCondition) { super(); this.hoverEnabled = false; this._mouseDownPos = new Vector2(); this._onPointerMove = (event) => { if (event.isPrimary === false) return; this.updateMouseFromEvent(event); if (this.hoverEnabled) this.hoverObject = this.checkIntersection()?.intersects[0].object ?? null; }; this._onPointerLeave = (event) => { if (event.isPrimary === false) return; this.domElement.style.cursor = this.cursorStyles.default; // this.updateMouseFromEvent(event); if (this.hoverEnabled || this.hoverObject) this.hoverObject = null; }; this._onPointerEnter = (_) => { // todo dispatch event? }; this._onPointerCancel = (_) => { // todo dispatch event? }; this._onPointerDown = (event) => { if (event.isPrimary === false) return; this.domElement.style.cursor = this.cursorStyles.down; this._mouseDownTime = this.time; this._mouseDownPos.copy(this.mouse); return undefined; }; this._onPointerUp = (event) => { 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; }; this._onPointerClick = (event) => { 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; }; this._root = root; this._camera = camera; this.domElement = domElement; this._time = this.time; this._mouseDownTime = 0; this._mouseUpTime = 1; this.selectionCondition = selectionCondition ?? ((selectedObject) => { 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() { return this._selected.length > 0 ? this._selected[0] : null; } set selectedObject(object) { this._setSelected(object); } _setSelected(object, 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() { return this._hovering.length > 0 ? this._hovering[0] : null; } set hoverObject(object) { 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; } updateMouseFromEvent(event) { 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; } checkIntersection() { const camera = this._camera; if (!camera) return null; this.raycaster.setFromCamera(this.mouse, camera); let intersects = this.raycaster.intersectObject(this._root, true); const uniqueIds = []; 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 = null; let intersect; 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. } } /** * Time threshold for a pointer click event */ ObjectPicker.PointerClickMaxTime = 200; /** * Distance threshold for a pointer click event */ ObjectPicker.PointerClickMaxDistance = 0.1; // 1/20 of the canvas //# sourceMappingURL=ObjectPicker.js.map