UNPKG

awv3

Version:
605 lines (550 loc) 25.2 kB
import * as THREE from 'three'; import MaterialStore from '../../misc/materialstore'; 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'; const epsilon = 1e-4; /** * @class MaterialSelector is used to select faces, edges, lines or points * (Filter settings: Mesh, LineSegments, Points) * It includes single selection as well as rubberband selection. This selector work * only for materials with meta-data. If the selection-filter includes 'Point', * then point representations are generated for LineSegments and Regions in the * scene and scaled to achieve the desired radius (pointRadiusPx). */ export class MaterialSelector { constructor(session) { 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 some of the original material properties this.materialStore = new MaterialStore(); // stores the material(s) of the selected object(s) this.selectedSet = new Set(); //the unobserve function is assigned when selector is activated //and must be called when deactivated this.unobserve = undefined; // initial size of point geometry this.pointSize = 1; // desired radius of point sphere in px this.pointRadiusPx = 5; // desired tolerance for picking lines this.linePrecisionPx = 5; // property changes for hovered materials this.hoveredProps = { color: new THREE.Color(0x28d79f), opacity: 1, linewidth: 3 }; // property changes for selected materials this.selectedProps = { color: new THREE.Color(0xc23369), opacity: 1, linewidth: 3 }; } /** * Deactivates the material-selector. Observation of selection-element is stopped. * Rubberband selection handlers are removed. */ deactivate() { this.removeAll(false); this.shiftHandler && document.removeEventListener('keydown', this.shiftHandler); this.shiftHandler = undefined; this.element = undefined; this.unobserve && this.unobserve(); this.points && this.points.destroy(); } /** * Activates the material-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) { //observe changes in element's children 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(material => { if (deletedItems[i] === material.meta.id) { candidates.push(material); } }); } candidates.forEach(material => this.remove({ material: material })); }); }); this.element = element; // Create new group for points this.points = new Object3(); this.session.pool.add(this.points); // check for multiple points with same position this.positions = []; // Create points, center points, etc if (element.types.includes('Point')) { this.session.pool.traverse(item => { if (item.type === 'Region') { item.points.forEach(obj => { if (this.session.isVisibleLayer(obj.meta.layer)) { this.addPoint(obj.meta.position, { ...obj.meta, id: obj.id }); } }); } else if (item.type === 'LineSegments' && item.interactive) { item.material.forEach(line => { if (!this.session.isVisibleLayer(line.meta.layer)) return; switch (line.meta.type) { case 'circle': case 'arc': // center point will be added when a circle/arc is selected break; case 'line': let start = item.localToWorld(line.meta.start); let end = item.localToWorld(line.meta.end); this.addPoint(start, { ...line.meta, id: String(line.meta.id) + '_s', position: line.meta.start, type: 'point', }); this.addPoint(end, { ...line.meta, id: String(line.meta.id) + '_e', position: line.meta.end, type: 'point', }); break; } }); } }); } this.points.children && this.points.children.forEach(point => point.setRenderOrder({ Mesh: 0, LineSegments: 100 })); // restore previously selected elements if (this.element.children.length > 0) { this.session.pool.traverse(obj => { if (obj.material) { let isMultiMaterial = Array.isArray(obj.material); if (isMultiMaterial) { obj.material.forEach(mat => { if (mat.meta && this.element.children.indexOf(mat.meta.id) > -1) { this.add({ material: mat }, false); } }); } else if (obj.material.meta && this.element.children.indexOf(obj.material.meta.id) > -1) { this.add({ material: obj.material }, false); } } }); } let types = ['Scene']; if (Array.isArray(element.types)) types = [...types, ...element.types]; else if (typeof element.types === 'function') types = object => object.type === 'Scene' || element.types(object); // 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.view.scene.traverseMaterials((material, item) => { // let value = Object3.RenderOrder.MeshesFirst[item.type]; // if (value !== undefined) item.renderOrder = value; //project bounding box to view coordinates if (item.visible && item.interactive && material.meta && types.includes(item.type)) { item.updateMatrixWorld(true); let minPt = material.meta.box.min.clone(); //TODO: get all 8 points let maxPt = material.meta.box.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.__material = material; 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, material: box.__material, meta: box.__material.meta, }); } } // Remove items not present any longer for (let item of candidates) { let found = false; for (let newItem of newCandidates) { if (item.material.meta.id === newItem.material.meta.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.material.meta.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 material 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(); } if (this.isSelected(event.material)) return event; switch (event.material.meta.type) { // add a center point when a circle/arc is selected case 'circle': case 'arc': this.addPoint( event.material.meta.center, { ...event.material.meta, id: String(event.material.meta.id) + '_c', position: event.material.meta.center, type: 'point', }, 0.1, ); break; // deselect all circles/arcs when a center point is selected case 'point': for (let material of this.selectedSet) switch (material.meta.type) { case 'circle': case 'arc': if (material.meta.center.distanceToSquared(event.material.meta.position) < epsilon * epsilon) this.remove({material}, notify); break; } break; } this.select(event.material); this.selectedSet.add(event.material); notify && this.session.store.dispatch(elementActions.update(this.element.id, { children: this.getSelectedIds() })); return event; } /** * Remove material 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.material)) return; this.unselect(event.material); this.selectedSet.delete(event.material); notify && this.session.store.dispatch(elementActions.removeChild(this.element.id, event.material.meta.id)); } /** * Remove all items (Material) from the set of selected elements * @param {Boolean} keepLocals - If true, selected ids are kept within the selection-element */ removeAll(removeElements = true) { this.selectedSet.forEach(material => this.unselect(material)); this.selectedSet.clear(); if (removeElements) { this.session.store.dispatch(elementActions.removeAllChilds(this.element.id)); } } /** * Called by selector when event is triggered. */ hover(event) { if (this.shiftHandler.handled) return; this.session.pool.view.setCursor('pointer'); if (event.material && !this.isSelected(event.material)) this.materialStore.store(event.material, this.hoveredProps).start(500); } /** * Called by selector when event is triggered. */ unhover(event) { if (this.shiftHandler.handled) return; if (event.material){ if(this.isSelected(event.material)) { this.materialStore.storedPropertiesMap.get(event.material).refcount--; } else { this.materialStore.restore(event.material).start(500); } } } /** * Called by selector when event is triggered. */ clicked(event) { if (this.shiftHandler.handled) return; if (!event.material.meta) return; if (this.isSelected(event.material)) { 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) { this.scalePoints(); this.calculateLinePrecision(); } /** * Highlight the selected material. Store original material for unhighlight. * @param {THREE.Material} - Material to select. */ select(material) { this.materialStore.store(material, this.selectedProps).start(500); } /** * Unhighlights the selected material, restoring the original appearance. * @param {THREE.Material} - Material to unselect. */ unselect(material) { if (this.view.interaction.hitsArray.find(hit => hit.material === material)) { this.materialStore.store(material, this.hoveredProps).start(500); } else { this.materialStore.restore(material).start(500); } } /** * Check if material object is already selected * @return {bool} TRUE if selected, FALSE otherwise */ isSelected(material) { return this.selectedSet.has(material); } /** * Returns the selected ids in an array * @return {array} 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.value.meta.id); item = iter.next(); } return selectedIds; } /** * Returns the selected elements in an array * @return {array} 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; } /** * Scales all the created points to the desired radius (pointRadiusPx) */ scalePoints() { if (this.points && this.points.children.length > 0) { this.points.children.forEach(point => { const scaleFactor = this.session.pool.view.calculateScaleFactor(point.position, this.pointRadiusPx); point.scale.set(scaleFactor, scaleFactor, scaleFactor); }); } } /** * Add a point to the points object in the scene. Duplicate points are * filtered out. * @param {THREE.Vector3} position - Position of the point to be added. * @param {object} meta - Meta-data of the object/material this point belongs to. */ addPoint(position, meta, opacity = 0) { if (this.isDuplicatePoint(position)) { return; } const geometry = new THREE.SphereBufferGeometry(this.pointSize, 32, 32); geometry.computeBoundingSphere(); geometry.computeBoundingBox(); // material const material = new THREE.MeshBasicMaterial({ color: 0x28b4d7, transparent: true, opacity }); // point as sphere-mesh const point = new THREE.Mesh(geometry, material); point.updateParentMaterials = false; point.renderOrder = 10000; point.type = 'Point'; point.position.copy(position); const vec = new THREE.Vector3(this.pointSize, this.pointSize, this.pointSize); const box = new THREE.Box3(position.sub(vec), position.add(vec)); point.userData.meta = material.meta = meta; const scaleFactor = this.session.pool.view.calculateScaleFactor(point.position, this.pointRadiusPx); point.scale.set(scaleFactor, scaleFactor, scaleFactor); this.points.add(point); this.positions.push(position); } /** * Checks for existing points within proximity of given position * @return true if a point exists already for the given position (within tolerance epsilon) */ isDuplicatePoint(position) { for (let hasposition of this.positions) if (hasposition.distanceToSquared(position) < epsilon * epsilon) return true; return false; } /** * Calculate and set the linePrecision the given position based on the given * precision in pixel. */ calculateLinePrecision() { if (this.session.pool.view && this.session.pool.scene.bounds.sphere) { const center = this.session.pool.scene.bounds.sphere.center; const point2 = this.session.pool.view.getPoint2(center.clone()); let screenPoint1 = new THREE.Vector3(0, 0, point2.z); let screenPoint2 = new THREE.Vector3(0, 0, point2.z); screenPoint2.x = this.linePrecisionPx; let worldPoint1 = this.session.pool.view.getPoint3( screenPoint1, this.session.pool.view.camera, screenPoint1.z, ); let worldPoint2 = this.session.pool.view.getPoint3( screenPoint2, this.session.pool.view.camera, screenPoint2.z, ); this.session.pool.view.interaction.raycaster.linePrecision = worldPoint1.sub(worldPoint2).length(); } } /** * 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({ material: element })); } }