awv3
Version:
⚡ AWV3 embedded CAD
422 lines (381 loc) • 16.8 kB
JavaScript
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 }));
}
}