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