awv3
Version:
⚡ AWV3 embedded CAD
278 lines (243 loc) • 11.6 kB
JavaScript
import * as THREE from 'three';
import Raycaster from '../three/raycaster';
import Object3 from '../three/object3';
export default class Interaction {
constructor(view = Error.log('View undefined'), options = {}) {
this.view = view;
this.canvas = view.canvas;
this.raycaster = new Raycaster(this);
this.enabled = typeof options.enabled !== 'undefined' ? options.enabled : true;
this.targets = [];
this.intersects = [];
this.hits = {};
this.hitsArray = [];
this.actions = {};
this.actionsArray = [];
this.hovered = {};
this.filter = undefined;
this.coordinates = new THREE.Vector3(0, 0, 0);
this.click = new THREE.Vector3(0, 0, 0);
this.delta = 0;
this.__hovered = [];
this.__frontside = {};
}
update() {
if (!this.enabled) return;
for (let target of this.targets) {
if (
target.interaction &&
target.interaction.enabled &&
(this.view.invalidateFrames > 0 ||
this.view.renderAlways ||
this.view.controls.inMotion) /*|| this.view.hudInMotion TODO: there are several hids now */
) {
target.emit(Object3.Events.Lifecycle.Rendered);
}
}
}
onMouseMove(event) {
if (!this.enabled) return;
this.coordinates.set(event.offsetX / this.view.width * 2 - 1, -(event.offsetY / this.view.height) * 2 + 1, 1);
this.delta = Math.max(
this.delta,
Math.abs(this.click.x - this.coordinates.x) + Math.abs(this.click.y - this.coordinates.y),
);
// View is moved or dry run (nothing under the curser when the view was clicked/touched)
if (this.view.controls.interaction || (!event.fake && this.view.input.mouse.down && this.actionsArray.length == 0))
return;
this.raycaster.setFromCamera(this.coordinates, this.view.camera.display);
this.intersects = [];
this.__hovered = [];
this.__frontside = {};
if (this.targets.length > 0) {
this.raycaster.castObjects(this.targets, this.intersects, this.filter);
if (this.intersects.length > 0) {
// Objects that have been pierced
for (let data of this.intersects) {
// Set unique key
data.id = data.receiver.id;
if (data.receiver.interaction.recursive) data.id += 'R' + data.object.id;
if (data.face && data.object.userData && data.object.userData.refs)
data.id += 'F' + data.face.materialIndex;
// Add region specific data
if (data.type === 'Region') {
data.id += 'V' + data.ref.id;
data.meta = data.ref.meta;
data.vertexId = data.ref.id;
}
// Flag is set on first occurance of a receiver
data.first = data === this.intersects[0];
// Current RAF timestamp
data.time = this.canvas.renderer.time;
// Mouse coordinates in world CS
data.ray = this.raycaster.ray;
// Fetch meta, if any
if (data.face && data.object.userData && data.object.userData.refs) {
data.meta = data.object.userData.refs[data.face.materialIndex];
} else if (data.object.userData && data.object.userData.meta) {
data.meta = data.object.userData.meta;
}
if (data.object.material) {
let isMultiMaterial = Array.isArray(data.object.material);
if (data.face) {
if (isMultiMaterial) {
data.materialIndex = data.face.materialIndex;
data.multiMaterial = data.object.material;
data.material = data.object.material[data.face.materialIndex];
} else {
data.material = data.object.material;
}
} else if (typeof data.index !== 'undefined') {
if (isMultiMaterial && data.object.geometry.groups) {
for (let group of data.object.geometry.groups) {
if (group.start <= data.index && group.start + group.count > data.index) {
data.materialIndex = group.materialIndex;
data.multiMaterial = data.object.material;
data.material = data.object.material[group.materialIndex];
data.meta = group.meta;
data.id += 'L' + group.materialIndex;
break;
}
}
} else {
data.material = data.object.material;
}
}
}
// Raycaster can return two intersections, front & back, we cast away the backside
if (this.__frontside[data.id]) continue;
// Mark it for hover if it hasn't been hit already
if ((!data.receiver.interaction.first || data.first) && !this.hovered[data.id])
this.__hovered.push(data);
this.hits[data.id] = data;
this.__frontside[data.id] = true;
}
} else {
this.actionsArray.length == 0 && this.view.setCursor();
}
// Take out everything that's not under the cursor right now and that's not in the actions
this.hitsArray = Object.keys(this.hits)
.map(item => this.hits[item])
// Sort after scene-graph
.sort((a, b) => a.receiver.depthIndex - b.receiver.depthIndex)
// Filter
.filter(data => {
let dispose =
(data.receiver.interaction.first && !data.first) || data.time != this.canvas.renderer.time;
if (dispose) {
if (this.hovered[data.id] && !this.actions[data.id]) {
delete this.hovered[data.id];
data.receiver.emit(Object3.Events.Interaction.Unhovered, data);
this.view.invalidate();
}
delete this.hits[data.id];
}
return !dispose;
});
// Hover items
this.__hovered.forEach(data => {
this.hovered[data.id] = true;
data.receiver.emit(Object3.Events.Interaction.Hovered, data);
this.view.invalidate();
});
}
// Object being dragged
if (this.delta > 0.01 && Object.keys(this.actions).length > 0) {
for (let data of this.actionsArray) {
if (!data.receiver.interaction.first || data.first) {
let vector = this.coordinates
.clone()
.unproject(this.view.camera.display)
.sub(this.view.camera.display.position)
.normalize();
data.drag = this.view.camera.display.position
.clone()
.add(vector.multiplyScalar(data.distance))
.add(data.offset);
data.ray = this.raycaster.ray;
data.receiver.emit(Object3.Events.Interaction.Dragged, data);
this.view.invalidate();
}
}
}
}
onMouseOut(event) {
if (!this.enabled) return;
for (let data of this.hitsArray) {
if (this.hovered[data.id] && !this.actions[data.id]) {
delete this.hovered[data.id];
data.receiver.emit(Object3.Events.Interaction.Unhovered, data);
this.view.invalidate();
}
delete this.hits[data.id];
}
}
onMouseDown(event) {
if (!this.enabled) return;
this.delta = 0;
this.click.set(event.offsetX / this.view.width * 2 - 1, -(event.offsetY / this.view.height) * 2 + 1, 1);
// Cause a fake-mousemove to refresh hits
this.onMouseMove({ ...event, fake: true });
this.actions = {};
this.actionsArray = [];
for (let data of this.hitsArray) {
if (!data.receiver.interaction.first || data.first) {
let pos = data.receiver.parent
? data.receiver.parent.localToWorld(data.receiver.position.clone())
: data.receiver.position.clone();
data.offset = pos.sub(data.point.clone());
this.actions[data.id] = data;
this.actionsArray.push(data);
data.receiver.emit(Object3.Events.Interaction.Picked, data);
}
}
this.view.invalidate();
}
onMouseUp(event) {
if (!this.enabled) return;
// Signal Mouseup
for (let data of this.actionsArray) {
if (this.hovered[data.id] && !this.hits[data.id]) {
delete this.hovered[data.id];
data.receiver.emit(Object3.Events.Interaction.Unhovered, data);
}
data.receiver.emit(Object3.Events.Interaction.Dropped, data);
if (this.delta < 0.05) {
data.receiver.emit(Object3.Events.Interaction.Clicked, data);
}
}
if (this.actionsArray.length == 0 && this.delta < 0.05) {
for (let target of this.targets) {
target.emit(Object3.Events.Interaction.Missed);
}
}
this.view.invalidate();
this.actions = {};
this.actionsArray = [];
}
removeTarget(obj) {
let index = this.targets.indexOf(obj);
if (index > -1) this.targets.splice(index, 1);
this.hitsArray.forEach(item => item.receiver === obj && this.hitsArray.splice(this.hitsArray.indexOf(item), 1));
this.actionsArray.forEach(
item => item.receiver === obj && this.actionsArray.splice(this.actionsArray.indexOf(item), 1),
);
Object.keys(this.hits).forEach(item => this.hits[item].receiver === obj && delete this.hits[item]);
Object.keys(this.actions).forEach(item => this.actions[item].receiver === obj && delete this.actions[item]);
Object.keys(this.hovered).forEach(item => this.hovered[item].receiver === obj && delete this.hovered[item]);
}
getActions() {
return this.actionsArray;
}
isHit(obj) {
return (
this.hitsArray.find(data => {
if (obj instanceof THREE.Material) return data.material === obj;
else return data.object.id === obj.id;
}) !== undefined
);
}
isAction(obj) {
return this.actionsArray.find(item => item.object.id == obj.id);
}
}