UNPKG

awv3

Version:
422 lines (381 loc) 16.8 kB
import * as THREE from 'three'; import Object3 from '../../three/object3'; import Hud from '../../core/hud'; import Orbit from '../../controls/orbit'; import CombinedCamera from '../../three/combinedcamera'; import Region from '../../three/region'; import { actions as elementActions } from '../store/elements'; import { arrayDiff } from '../helpers'; /** * @class ObjectSelector is used to select whole objects in the view * (Filtersettings: Object). * includes single selection as well as rubberband selection. Object selection * is based on the object's id only and works without userData and meta-information. */ export class ObjectSelector { constructor(session, options) { this.session = session; // Wait until the sessions pool has been added into the view (happens in <App/>) this.session.pool.viewFound().then(view => this.view = view); // the currently active selecion-element this.element = undefined; // stores the selected objects this.selectedSet = new Set(); // call unobserve when deactivated this.unobserve = undefined; } /** * Deactivates the object-selector. Observation of selection-element is stopped. * Rubberband selection handlers are removed. */ deactivate(element = this.element) { this.removeAll(false); this.shiftHandler && document.removeEventListener('keydown', this.shiftHandler); this.shiftHandler = undefined; this.element = undefined; this.unobserve && this.unobserve(); } /** * Activates the object-selector. The Given selection-element is observed in * order to register changes when click-deleting labels. Handlers for * rubberband selection are created. */ activate(element = this.element) { this.unobserve = this.session.observe( state => state.elements[element.id].children, (...args) => { arrayDiff( ...args, newItems => {}, deletedItems => { let candidates = []; for (let i = 0; i < deletedItems.length; i++) { this.selectedSet.forEach(object => { if (deletedItems[i] === object.id) { candidates.push(object); } }); } candidates.forEach(object => this.remove({ object })); }, ); }, ); this.element = element; this.view.interaction.filter = [this.session.pool]; // restore previously selected objects if (this.element.children.length > 0) { this.session.pool.traverse(obj => { if (this.element.children.indexOf(obj.id) > -1) { this.add({ object: obj }, false); } }); } // update materials this.session.pool.traverse(item => item.updateMaterials()); // SHIFT KEY selection handler let candidates = []; this.shiftHandler = event => { if (!this.shiftHandler.handled && event.keyCode === 16) { this.shiftHandler.handled = true; this.view.input.debounce = false; this.view.controls.enabled = false; this.view.interaction.enabled = false; let boundingBoxes2D = []; this.session.pool.traverse(item => { //project bounding box to view coordinates if (item.visible && item.interactive && item.geometry) { item.updateMatrixWorld(true); item.geometry.computeBoundingBox(); let minPt = item.geometry.boundingBox.min.clone(); //TODO: get all 8 points let maxPt = item.geometry.boundingBox.max.clone(); //TODO: get all 8 points //project to view-coordinates let p1 = this.view.getPoint2(minPt); let p2 = this.view.getPoint2(maxPt); //bounding box in 2D let bounds2D = new THREE.Box2(); bounds2D.setFromPoints([p1, p2]); bounds2D.__object = item; boundingBoxes2D.push(bounds2D); } }); // Init new heads up display, add it to the view this.hud = new Hud(this.view); this.view.addHud(this.hud); this.hud.controls = undefined; this.hud.camera = new CombinedCamera({ near: this.view.camera.near, far: this.view.camera.far, fov: this.view.camera.fov, }); this.view.measure(true); let modelBounds = this.view.scene.updateBounds().bounds; let geom = new THREE.PlaneGeometry(1, 1); let box = new THREE.Mesh( geom, new THREE.MeshBasicMaterial({ transparent: true, opacity: 0.05, color: new THREE.Color(0), }), ); this.hud.scene.add(box); let clickDown, mousePos, rubberBandLine, clickHud; let handler = event => { switch (event.type) { case 'mousedown': clickDown = new THREE.Vector2(event.offsetX, event.offsetY); mousePos = new THREE.Vector2(event.offsetX + 11, event.offsetY + 1); clickHud = this.view.getPoint3( { x: event.offsetX, y: event.offsetY }, this.hud.camera.display, 0, ); box.position.copy(clickHud); break; case 'mousemove': if (!clickDown) return; //update rectangle let tempPoint = this.view.getPoint3( { x: event.offsetX, y: event.offsetY }, this.hud.camera.display, 0, ); let delta = tempPoint.sub(clickHud); let width = Math.abs(delta.x); let height = Math.abs(delta.y); let halfHeight = height / 2; let halfWidth = width / 2; let halfDeltaX = delta.x / 2; let halfDeltaY = delta.y / 2; geom.vertices[0].set(-halfWidth + halfDeltaX, halfHeight + halfDeltaY, 0); geom.vertices[1].set(width - halfWidth + halfDeltaX, halfHeight + halfDeltaY, 0); geom.vertices[2].set(-halfWidth + halfDeltaX, -(height - halfHeight) + halfDeltaY, 0); geom.vertices[3].set( width - halfWidth + halfDeltaX, -(height - halfHeight) + halfDeltaY, 0, ); geom.verticesNeedUpdate = true; mousePos = new THREE.Vector2(event.offsetX, event.offsetY); //check intersection with rectangle in display-coordinates let rubberBandRectangle = new THREE.Box2(); rubberBandRectangle.setFromPoints([clickDown, mousePos]); let newCandidates = []; for (let box of boundingBoxes2D) { if (rubberBandRectangle.containsBox(box)) { // create new object identical to raycast-hit newCandidates.push({ distance: 0, point: undefined, face: undefined, faceIndex: undefined, indices: undefined, object: box.__object, }); } } // Remove items not present any longer for (let item of candidates) { let found = false; for (let newItem of newCandidates) { if (item.object.id === newItem.object.id) { found = true; break; } } if (!found) { this.remove(item, false); } } candidates = newCandidates; // Add new items, but only if they aren't known yet for (let item of candidates) this.add(item, false); this.view.invalidate(); break; case 'mouseup': box && box.destroy(); let tempIds = []; candidates.forEach(item => tempIds.push(item.object.id)); clickDown = undefined; if (rubberBandLine != undefined) { this.view.scene.remove(rubberBandLine); rubberBandLine = undefined; } // Update UI after mouse-up this.session.store.dispatch( elementActions.update(this.element.id, { children: candidates.map(item => item.material.meta.id), }), ); break; } }; this.view.input.on(['mousedown', 'mousemove', 'mouseup'], handler); this.tempHandler = () => { // Remove and destroy heads up display this.view.removeHud(this.hud); this.hud.destroy(); this.shiftHandler.handled = false; this.view.input.removeListener(['mousedown', 'mousemove', 'mouseup'], handler); document.removeEventListener('keyup', this.tempHandler); this.view.input.debounce = true; this.view.controls.enabled = true; this.view.interaction.enabled = true; }; document.addEventListener('keyup', this.tempHandler); } }; this.shiftHandler.handled = false; document.addEventListener('keydown', this.shiftHandler); } /** * Add object to the set of selected elements * @param {RayCast-Hit} event - Object generated by the raycaster including the * hit object, material and meta-information */ add(event, notify = true) { // If the elements limit is met, all prior selections should be removed if (this.selectedSet.size >= this.element.limit) { this.removeAll(notify); } if (!this.isSelected(event.object)) { this.select(event.object); this.selectedSet.add(event.object); notify && this.session.store.dispatch(elementActions.addChild(this.element.id, event.object.id)); } return event; } /** * Remove object from the set of selected elements * @param {RayCast-Hit} event - Object generated by the raycaster including the * hit object, material and meta-information */ remove(event, notify = true) { if (this.isSelected(event.object)) { this.unselect(event.object); this.selectedSet.delete(event.object); notify && this.session.store.dispatch(elementActions.removeChild(this.element.id, event.object.id)); } } /** * Remove all items (three object) from the set of selected elements * @param {Boolean} keepLocals - If true, selected ids are kept within the selection-element */ removeAll(notify = true) { this.selectedSet.forEach(object => this.unselect(object)); this.selectedSet.clear(); notify && this.session.store.dispatch(elementActions.removeAllChilds(this.element.id)); } /** * Called by selector when event is triggered. */ hover(event) { this.session.pool.view.setCursor('pointer'); if (event.object.materials && !this.isSelected(event.object)) { let anim = event.object.animate({ materials: { meshes: [{ color: new THREE.Color(0x28d79f), opacity: 1 }] }, }); if (event.object.__origProps === undefined) { event.object.__origProps = anim.getProperties(); } anim.start(0); } } /** * Called by selector when event is triggered. */ unhover(event) { if (event.object.materials && event.object.__origProps && !this.isSelected(event.object)) { event.object.animate(event.object.__origProps).start(500); } } /** * Highlight the selected object. Store original material for unhighlight. */ select(object) { if (object.materials) { let anim = object.animate({ materials: { meshes: [{ color: new THREE.Color(0xc23369), opacity: 1 }] } }); if (object.__origProps === undefined) { object.__origProps = anim.getProperties(); } anim.start(500); } } /** * Unighlight the object using hover material. */ unselect(object) { if (this.view.interaction.isHit(object)) { // If the object is underneath the curser it should be highlighted object.animate({ materials: { meshes: [{ color: new THREE.Color(0x28d79f), opacity: 1 }] } }).start(0); } else if (object.materials && object.__origProps) { // ... otherwise it should return to its proper color object.animate(object.__origProps).start(0); } } /** * Called by selector when event is triggered. */ clicked(event) { if (this.shiftHandler.handled) return; if (this.isSelected(event.object)) { this.remove(event); } else { this.add(event); } } /** * Called by selector when event is triggered. */ missed(event) { this.removeAll(true); } /** * Called by selector when event is triggered. */ rendered(event) {} /** * Check if object is already selected * @return TRUE if selected, FALSE otherwise */ isSelected(object) { return this.selectedSet.has(object); } /** * Returns the selected ids in an array * @return selectedIds - Array with the selected ids */ getSelectedIds() { let iter = this.selectedSet.values(); let selectedIds = []; let item = iter.next(); while (item.value !== undefined) { selectedIds.push(item.id); item = iter.next(); } return selectedIds; } /** * Returns the selected elements in an array * @return selectedElements - Array with the selected elements */ getSelectedElements() { let iter = this.selectedSet.values(); let selectedElements = []; let item = iter.next(); while (item.value !== undefined) { selectedElements.push(item.value); item = iter.next(); } return selectedElements; } /** * Removes the given elements from the current selection. * @param {array} elements - elements to be removed from the selection. */ unselectElements(elements) { elements.forEach(element => this.remove({ object: element })); } }