import { Reporter } from '../reporter/reporter.js';
import { InformationSource } from '../reporter/information_source.js';
import { Camera, PerspectiveCamera, OrthographicCamera, Box3, Scene, BoxHelper, WebGLRenderer, Group, Raycaster, Box3Helper, Euler, Plane, Vector2, Vector3, Quaternion, BoxGeometry, MeshBasicMaterial, Mesh, LinearEncoding, LinearFilter, LinearToneMapping, RGBAFormat, FloatType, Color } from '../../node_modules/three/build/three.module.js';
import { checkPropTypes, UUIDRegex } from '../lib.js';
import { Timer } from '../timer.js';
import { Project } from '../project.js';
import { Block } from '../package/block/block.js';
import { WrappedMaterial } from '../package/material/wrapped_material.js';
import { Component } from '../component/component.js';
import { DimensionFrame } from './dimension_frame.js';
import defaultCameraSettings from '../default_settings/camera.js';
import { CSS2DRenderer } from '../../node_modules/three/examples/jsm/renderers/CSS2DRenderer.js';
import { CSS2DObject } from '../../node_modules/three/examples/jsm/renderers/CSS2DRenderer.js';
import { v4 as uuid } from '../../node_modules/uuid/dist/esm-browser/index.js';
import { ContactShadow } from '../scene/contact_shadow.js'
// Post Processing
import { Pass } from '../../node_modules/three/examples/jsm/postprocessing/Pass.js';
import { RenderPass } from '../../node_modules/three/examples/jsm/postprocessing/RenderPass.js';
import { EffectComposer } from '../../node_modules/three/examples/jsm/postprocessing/EffectComposer.js';
import { ShaderPass } from '../../node_modules/three/examples/jsm/postprocessing/ShaderPass.js';
import { UnrealBloomPass } from '../../node_modules/three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { TAARenderPass } from '../../node_modules/three/examples/jsm/postprocessing/TAARenderPass.js';
// Shaders
import { GammaCorrectionShader } from '../../node_modules/three/examples/jsm/shaders/GammaCorrectionShader.js';
import { CopyShader } from '../../node_modules/three/examples/jsm/shaders/CopyShader.js';
import { FXAAShader } from '../../node_modules/three/examples/jsm/shaders/FXAAShader.js';
import * as THREE from '../../node_modules/three/build/three.module.js';
import { Actor } from '../actor/actor.js';
import { BlockInstance } from '../configurator/block_instance.js';
import { WrappedImage } from '../package/image/wrapped_image.js';
import { WrappedMesh } from '../package/mesh/wrapped_mesh.js';
import { Tween } from './tween.js';
import CameraControls from '../../node_modules/camera-controls/dist/camera-controls.module.js';
import { Marker } from './marker.js';
import { Configuration } from '../configurator/configuration.js';
import { Configurator } from '../configurator/configurator.js';
import { ConsoleLogger } from '../reporter/console_logger.js';
import { Geometry } from '../package/geometry/geometry.js';
//import { getDimensions } from '../utilities/getDimensions.js';
//import{ drawDimensionsLine, updateLabels } from '../utilities/drawDimensionsLine.js';
CameraControls.install({ THREE: THREE });
/**
* @note
* Op dit moment gebruikt elke view dezelfde renderer zodat de PMREM map gedeeld wordt
* maar dat is vrij annoying, omdat 1 renderer ook 1 canvas heeft. Zo is het idee van meerdere
* views overbodig (anders dan camerastandpunt). Misschien toch meerdere material versies builden,
* bijv het 'original' en een voor elke view die een PMREM wil gebruiken...
*/
/**
* View base class
* @event View#blockinstancehover
* @event View#pointerdown
* @event View#fastclick
* @event View#click
* @event View#dragover
* @event View#drop
*/
class View extends InformationSource {
/**
* @param {Reporter} reporter
* @param {Object} settings
* @param {UUID} [settings.id]
* @param {string} [settings.name]
* @param {Scene} settings.scene
* @param {WebGLRenderer} settings.renderer
* @param {WebGLRenderer} settings.rendererOrtho
* @param {Timer} settings.timer
* @param {Object<string,any>} settings.cameraSettings
* @param {Array<Pass>} settings.passes
* @param {Function} settings.findConfigurator
* @param {Function} settings.addConfigurator
* @param {Function} settings.findComponentById
*/
constructor(reporter, settings) {
super(reporter, settings);
checkPropTypes(
settings,
{
scene: Scene,
renderer: WebGLRenderer,
rendererOrtho: WebGLRenderer,
timer: Timer
},
{
rendererSettings: Object,
cameraSettings: Object,
passes: val => {
if (!Array.isArray(val)) {
return 'Not an array';
}
for (let i = 0; i < val.length; i += 1) {
const entry = val[i];
if (!(entry instanceof Pass)) {
return `Entry ${i} is not a Pass but a ${entry.constructor.name}`;
}
}
return true;
}
}
);
const view = this;
this.markers = {};
this.dimensionFrames = {};
this.domElementDimensions;
this.camTween = null;
this.tweens = {};
this.scene = settings.scene;
this.renderer = settings.renderer;
this.rendererOrtho = settings.rendererOrtho;
this.timer = settings.timer;
this.cameraSettings = {
...defaultCameraSettings,
...(settings.cameraSettings || {})
};
this.perspectiveCamera = new PerspectiveCamera(
this.cameraSettings.fov,
this.cameraSettings.aspect,
this.cameraSettings.near,
this.cameraSettings.far,
);
this.perspectiveCamera.layers.enable(Project.layerMap.visibleActors);
this.perspectiveCamera.layers.disable(Project.layerMap.hiddenActors);
this.orthographicCamera = new OrthographicCamera();
this.domElement = document.createElement('div');
this.domElement.classList = ['canvas-container']
this.domElement.appendChild(this.renderer.domElement);
this.renderPass = new RenderPass(this.scene, this.perspectiveCamera);
this.renderPass.name = 'Default Render Pass';
this.renderPass.clearAlpha = 1;
this.passes = [
this.renderPass, // deze pass moet als eerste komen!
...(settings.passes || [])
];
// console.log(this.passes)
this.scene.add(this.perspectiveCamera);
this.scene.add(this.orthographicCamera);
//css 2D Renderer
this.css2dRenderer = new CSS2DRenderer();
this.css2dRenderer.domElement.style.position = 'absolute';
this.css2dRenderer.domElement.style.top = '0px';
this.css2dRenderer.domElement.style.outline = 'none';
this.css2dRenderer.id = 'css2dRenderer'
this.on(
'dimensionsupdate',
newDimensions => {
this.css2dRenderer.setSize(
newDimensions.width,
newDimensions.height
);
}
);
this.domElement.appendChild(this.css2dRenderer.domElement);
this.clock = new THREE.Clock();
this.cameraControls = new CameraControls(this.perspectiveCamera, this.domElement);
// Track all DOM event listeners for cleanup in destroy()
this._domListeners = [];
const trackListener = (target, type, handler, options) => {
target.addEventListener(type, handler, options);
this._domListeners.push({ target, type, handler });
};
// limit lowest camera angle
const maxPolarAngle = this.cameraSettings?.maxPolarAngle ?? 180;
this.cameraControls.maxPolarAngle = THREE.MathUtils.degToRad(maxPolarAngle)
this.cameraControls.normalizeRotations()
trackListener(this.cameraControls, 'control', evt => {
this.timer.addUpdateRequest(this.id, 'Camera controls control');
});
trackListener(this.cameraControls, 'controlstart', evt => {
this.timer.addUpdateRequest(this.id, 'Camera controls start');
});
trackListener(this.cameraControls, 'wake', evt => {
this.cameraControlsAwake = true;
this.timer.addUpdateRequest(this.id, 'Camera controls wake');
});
trackListener(this.cameraControls, 'sleep', evt => {
this.cameraControlsAwake = false;
this.timer.removeUpdateRequest(this.id);
});
this.cameraControls.setLookAt(-2, 1, 6, 0, 0.3, 0, false);
//Shadow
this.shadow = new ContactShadow(reporter, {
scene: this.scene,
camera: this.perspectiveCamera,
renderer: this.renderer
});
this.shadow._build();
this.scene.add(this.shadow._shadowGroup);
this.scene.castShadow = true;
this.scene.receiveShadow = true;
this.shadow._update();
//Methods
this.findConfigurator = settings.findConfigurator
this.addConfigurator = settings.addConfigurator
this.findComponentById = settings.findComponentById
this.addEvent('touchstart');
this.addEvent('touchmove');
this.addEvent('touchend');
this.addEvent('touchcancel');
this.addEvent('click');
this.addEvent('dblclick');
this.addEvent('fastclick');
this.addEvent('pointerdown');
this.addEvent('pointerup');
this.addEvent('pointermove');
this.addEvent('dragenter');
this.addEvent('dragover');
this.addEvent('dragleave');
this.addEvent('drop');
this.addEvent('dragend');
this.addEvent('componentDragEnter')
this.addEvent('componentDragLeave')
this.addEvent('blockinstancehover');
this.addEvent('componenthoverstart');
this.addEvent('componenthoverend');
this.addEvent('dimensionsupdate');
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#multiple_identical_event_listeners
const viewBoundHoverEmitter = this.emitIntersectedBlocks.bind(this, ['componenthoverstart', 'componenthoverend']);
const viewBoundDragEmitter = this.emitIntersectedBlocks.bind(this, ['componentdragenter', 'componentdragleave']);
this.on(
'listenerChange',
({ event, listenerObject }) => {
switch (event) {
case 'blockinstancehover':
case 'componenthoverstart':
case 'componenthoverend':
if (Object.keys(listenerObject).length > 0) {
this.domElement.addEventListener(
'pointermove',
viewBoundHoverEmitter
);
}
else {
this.domElement.removeEventListener(
'pointermove',
viewBoundHoverEmitter
);
}
break;
}
}
);
//Touch Event Listeners
this._boundOnTouchStart = this.onTouchStart.bind(this);
this._boundOnTouchMove = this.onTouchMove.bind(this);
this._boundOnTouchEnd = this.onTouchEnd.bind(this);
this._boundOnTouchCancel = this.onTouchCancel.bind(this);
trackListener(this.domElement, 'touchstart', this._boundOnTouchStart);
trackListener(this.domElement, 'touchmove', this._boundOnTouchMove);
trackListener(this.domElement, 'touchend', this._boundOnTouchEnd);
trackListener(this.domElement, 'touchcancel', this._boundOnTouchCancel);
//Click Event Listeners
this._boundOnClick = this.onClick.bind(this);
this._boundOnDoubleClick = this.onDoubleClick.bind(this);
trackListener(this.domElement, 'click', this._boundOnClick);
trackListener(this.domElement, 'dblclick', this._boundOnDoubleClick);
trackListener(this.domElement, 'fastclick', event => { console.log('fast click'); });
//Pointer Events Listeners
this._boundOnPointerUp = this.onPointerUp.bind(this);
this._boundOnPointerDown = this.onPointerDown.bind(this);
this._boundOnPointerMove = this.onPointerMove.bind(this);
trackListener(document, 'pointerup', this._boundOnPointerUp);
trackListener(document, 'pointerdown', this._boundOnPointerDown);
trackListener(document, 'pointermove', this._boundOnPointerMove);
//Drag Event Listeners (Default)
this._boundOnDragEnter = this.onDragEnter.bind(this);
this._boundOnDragOver = this.onDragOver.bind(this);
this._boundOnDrop = this.onDrop.bind(this);
this._boundOnDragLeave = this.onDragLeave.bind(this);
this._boundOnDragEnd = this.onDragEnd.bind(this);
trackListener(this.domElement, 'dragenter', this._boundOnDragEnter);
trackListener(this.domElement, 'dragover', this._boundOnDragOver);
trackListener(document, 'drop', this._boundOnDrop);
trackListener(document, 'dragleave', this._boundOnDragLeave);
trackListener(document, 'dragend', this._boundOnDragEnd);
//Drag Event Listeners (PB)
this._boundOnComponentDragEnter = this.onComponentDragEnter.bind(this);
this._boundOnComponentDragLeave = this.onComponentDragLeave.bind(this);
trackListener(document, 'componentdragenter', this._boundOnComponentDragEnter);
trackListener(document, 'componentdragleave', this._boundOnComponentDragLeave);
//Window Event Listeners
this._boundUpdateDOMElementDimensions = this.updateDOMElementDimensions.bind(this);
trackListener(window, 'resize', this._boundUpdateDOMElementDimensions);
//Keyboard Event Listeners
this._boundOnKeyUp = this.onKeyUp.bind(this);
trackListener(document, 'keyup', this._boundOnKeyUp);
this.rebuildComposer();
// Th next looks dirtier than it is, the view is added by an external application
// which will, in all likeliness, add the canvas to some container directly
// after creation. This means that the canvas's dimensions are pbly not available
// right now, but will be in the next tick.
setTimeout(this.updateDOMElementDimensions.bind(this), 1);
}
cameraControlsAwake = false;
/**
* Data about the last mousedown event on the output canvas
* @type {Object}
* @property {number} x
* @property {number} y
* @property {number} timestamp
*/
lastPointerDown = { x: 0, y: 0, timestamp: 0 };
/**
* Data about the last mouseup event on the output canvas
* @type {Object}
* @property {number} x
* @property {number} y
* @property {number} timestamp
*/
lastPointerUp = { x: 0, y: 0, timestamp: 0 };
/**
* Data about the last mousemove event on the output canvas
* @type {Object}
* @property {number} x
* @property {number} y
*/
lastPointerMove = { x: 0, y: 0 };
/**
* The 'previous' camera position
* @type {Vector3}
*/
lastPerspectiveCameraPosition = new Vector3();
/**
* The 'previous' camera quaternion
* @type {Quaternion}
*/
lastPerspectiveCameraQuaternion = new Quaternion();
/** @type {Scene} */
scene;
/** @type {PerspectiveCamera} */
perspectiveCamera;
/** @type {Raycaster} */
rayCaster = new Raycaster();
//raycaster = new Raycaster();
/** @type {WebGLRenderer} */
renderer;
/** @type {Array<Pass>} */
passes;
/** @type {RenderPass} */
renderPass;
/** @type {Timer} */
timer;
/** @type {HTMLDivElement} */
domElement;
/** @type {DOMRect} */
domElementDimensions;
/** @type {Object<UUID,Tween>} */
tweens;
/** @type {Tween} */
camTween;
/**
* Finds the coordinates and dimensions of the output canvas,
* updates the camera and renderer accordingly and triggers one
* view update (including render)
*/
/**
* Remove all DOM event listeners and clean up resources.
* Prevents listener leaks when views are removed/replaced.
*/
destroy() {
// Remove all tracked DOM event listeners
if (this._domListeners) {
for (const { target, type, handler } of this._domListeners) {
target.removeEventListener(type, handler);
}
this._domListeners = [];
}
// Remove the renderer canvas from DOM
if (this.domElement && this.domElement.parentNode) {
this.domElement.parentNode.removeChild(this.domElement);
}
// Clean up camera controls
if (this.cameraControls) {
this.cameraControls.dispose();
}
// Remove from scene
if (this.scene && this.perspectiveCamera) {
this.scene.remove(this.perspectiveCamera);
}
if (this.scene && this.orthographicCamera) {
this.scene.remove(this.orthographicCamera);
}
// Clean up markers
this.removeAllMarkers();
// Clean up shadow
if (this.shadow) {
if (this.scene) {
this.scene.remove(this.shadow._shadowGroup);
}
}
// Remove timer update request
if (this.timer) {
this.timer.removeUpdateRequest(this.id);
}
// Clear references
this.markers = {};
this.dimensionFrames = {};
this.tweens = {};
this.scene = null;
this.renderer = null;
this.rendererOrtho = null;
this.css2dRenderer = null;
this.perspectiveCamera = null;
this.orthographicCamera = null;
this.cameraControls = null;
this.shadow = null;
this.timer = null;
this.findConfigurator = null;
this.addConfigurator = null;
this.findComponentById = null;
if (typeof super.destroy === 'function') {
super.destroy();
}
}
updateDOMElementDimensions() {
// Guard against destroyed view (domElement may be removed from DOM)
if (!this.domElement || !this.domElement.parentNode) {
return;
}
const newRect = this.domElement.getBoundingClientRect();
// console.log( newRect )
newRect.width = newRect.right - newRect.left;
newRect.halfWidth = newRect.width / 2;
newRect.height = newRect.bottom - newRect.top;
newRect.halfHeight = newRect.height / 2;
this.domElementDimensions = newRect;
this.perspectiveCamera.aspect = newRect.width / newRect.height;
this.perspectiveCamera.updateProjectionMatrix();
this.orthographicCamera.left = newRect.width / -2;
this.orthographicCamera.right = newRect.width / 2;
this.orthographicCamera.top = newRect.height / 2;
this.orthographicCamera.bottom = newRect.height / -2;
this.orthographicCamera.near = 0.1;
this.orthographicCamera.far = 1000;
this.orthographicCamera.position.set(0, 0, 10)
this.renderer.setSize(newRect.width, newRect.height);
this.rendererOrtho.setSize(newRect.width, newRect.height);
this.shadow._update()
this.rebuildComposer();
this.timer.trigger();
this.emit('dimensionsupdate', this.domElementDimensions);
}
/**
* Add a THREE.Pass to the front of the chain and rebuild the composer
* @param {Pass} pass
*/
prependPass(pass) {
this.passes = [pass, ...this.passes];
this.rebuildComposer();
}
/**
* Add a THREE.Pass to the end of the chain and rebuild the composer
* @param {Pass} pass
*/
appendPass(pass) {
this.passes = [...this.passes, pass];
this.rebuildComposer();
}
/**
* Rebuilds the Effect Composer that is used to produce renders
*/
rebuildComposer() {
this.composer = new EffectComposer(this.renderer); // misschien is dit niet nodig?
for (let pass of this.passes) {
this.composer.addPass(pass);
}
}
/**
* Tweens camera to a standard view point
* @param {Function} callback
* @param {number} lengthMs
* @param {Function} [easingFn = Tween.easeInOutCubic]
*/
tween(callback, lengthMs = 250, easingFn = Tween.easeInOutCubic) {
if (this.camTween instanceof Tween) {
this.camTween.stop()
this.camTween = null;
}
const tween = new Tween(lengthMs, callback, easingFn);
// this.tweens[ tween.id ] = tween;
this.camTween = tween;
tween.on('stop', () => { delete this.tweens[tween.id]; });
}
focusOn(target, quaternion, paddingX, paddingY) {
// get target boundingbox
// create vector with correct vector
// vector from bb center
// cam on vector
// calc distance (either directly or from projection)
// add tween
}
/**
* Called after the scene was changed by the engine, triggers
* a view update
*/
onSceneUpdate(args) {
// console.log('onSceneUpdate args', args)
Object.values(this.markers)
.forEach(marker => marker.relinkObject3D());
for (let dimFrame of Object.values(this.dimensionFrames)) {
dimFrame.onSceneUpdate();
}
this.update();
}
/**
* Updates the view (render) and orbit controls (if enabled)
* and removes the timer update request if the camera isn't moving
*/
update() {
this.lastPerspectiveCameraPosition.copy(this.perspectiveCamera.position);
this.lastPerspectiveCameraQuaternion.copy(this.perspectiveCamera.quaternion);
const delta = this.clock.getDelta();
const hasControlsUpdated = this.cameraControls.update(delta);
for (let tween of Object.values(this.tweens)) {
tween.update();
}
this.render();
//this.renderOrtho();
for (let marker of Object.values(this.markers)) {
marker.update();
}
for (let dimFrame of Object.values(this.dimensionFrames)) {
dimFrame.update();
}
const camMovement = this.lastPerspectiveCameraPosition.distanceTo(this.perspectiveCamera.position);
if (camMovement > 10e-5 || hasControlsUpdated) {
if (!this.timer.hasRequestId(this.id)) {
this.timer.addUpdateRequest(this.id);
}
// if ( this.cameraControlsAwake === false ) {
// this.timer.removeUpdateRequest(this.id);
// }
// if ( this.lastPerspectiveCameraPosition && this.lastPerspectiveCameraPosition.distanceTo(this.perspectiveCamera.position) < 10e-3) {
// console.log('no movement');
// this.timer.removeUpdateRequest(this.id);
// }
// else {
// // console.log('test', this.lastPerspectiveCameraPosition, this.perspectiveCamera.position);
// }
}
else if (this.timer.hasRequestId(this.id)) {
this.timer.removeUpdateRequest(this.id);
}
//create /update project boundingbox
//get the min and max of all configurations
// if ( this.configurators.length > 0){
// console.log( this.configurators[0].configuration )
// }
}
/**
* Render a new image from the scene and the perspective camera
*/
render() {
this.composer.render(this.scene, this.perspectiveCamera);
this.css2dRenderer.render(this.scene, this.perspectiveCamera);
}
renderOrtho() {
this.composer.render(this.scene, this.orthographicCamera);
this.css2dRenderer.render(this.scene, this.orthographicCamera);
}
/**
* Touch events
*/
onTouchStart(event){
console.log( 'touch start')
// const data = this.findFirstIntersectedBlockInstance() || {};
// data.event = event;
// this.emit('touch start', data);
}
onTouchMove(event){
//console.log( 'touch move')
}
onTouchEnd(event){
//console.log( 'touch end')
}
onTouchCancel(event){
//console.log( 'touch cancel')
}
/**
* Finds the block 'under' the current (last known) mouse coordinates
* and emits an event with that data, if anyone is listening
* @fires View#click
*/
onClick(event) {
this.dragActive = false;
if (Object.keys(this._events['click']).length > 0) {
const data = this.findFirstIntersectedBlockInstance() || {};
data.event = event;
this.emit('click', data);
}
}
onDoubleClick( event ){
// this.dragActive = false;
// this.onPointerDownActive = false;
console.log('double click')
if (Object.keys(this._events['click']).length > 0) {
const data = this.findFirstIntersectedBlockInstance() || {};
data.event = event;
this.emit('dblClick', data);
}
}
selectionActive = false; //deze wordt gebruikt om te bepalen of de selectie actief is of niet
/**
* Stores the pointer down coordinates and timestamp.
* Finds the block 'under' the current (last known) mouse coordinates
* and emits an event with that data, if anyone is listening
* @fires View#pointerdown
*/
onPointerDown(event) {
}
hideShadowOnDrag = false; //deze wordt bij pointer move 1x op tur gezet zodat de shaduw uitgezet kan wordeen tijdens het onPointerMove event
onPointerMove(event){
}
/**
* Stores the pointer up coordinates and timestamp.
* If the mouse hasnt move (a lot) since mouse down, it find the block
* 'under' the last known mouse coordinates and emits an event
* with that data, if anyone is listening
* @fires View#fastclick
*/
onPointerUp(event) {
}
/**
* Can receive data in the form of a coomponent.
* Evaluates the data and makes distinction between a wrapped material or a block.
* @fires View#dragenter
*/
async onDragEnter( event ){
}
/**
* Finds the block 'under' the current (last known) mouse coordinates
* when a drag is happening and emits an event with that data and the
* original dragover event
* @fires View#dragover
*/
async onDragOver(event) {
}
/**
* Finds the block 'under' the drop location
* and emits an event with that data and the drop event
* @fires View#drop
*/
async onDrop(event) {
}
onDragLeave(event){
}
onDragEnd(){
}
onComponentDragEnter(event){
console.log('componentdragenter')
// this.domElement.addEventListener(
// 'dragover',
// viewBoundDragEmitter
// )
}
onComponentDragLeave(event ){
if (Object.keys( listenerObject ).length > 0) {
//voorlopig uitgezetomdat dit conflicteert met de drag functies
// this.domElement.addEventListener(
// 'dragover',
// viewBoundDragEmitter
// );
}
else {
// this.domElement.removeEventListener(
// 'dragover',
// viewBoundDragEmitter
// );
}
}
onKeyUp(event){
}
/** @type {WrappedMesh} */
mouseoverMesh;
/** @type {BlockInstance} */
mouseoverBlockInstance;
/** @type {Actor} */
mouseoverActor;
/**
* Emits the 'blockinstancehover' event if the mouse is
* hovering over a blockInstance. It only 'looks for' the first
* intersection block.
* @fires View#blockinstancehover
*/
// zou het niet slimmer zijn om losse events te hebben voor confighover,
// componenthover, assignablehover?
emitIntersectedBlocks([startEventName, endEventName]) {
// console.log(startEventName)
const intersectedBlockInstanceData = this.findFirstIntersectedBlockInstance();
if (intersectedBlockInstanceData) {
const intersectedBlockInstance = intersectedBlockInstanceData.blockInstance;
// console.log(intersectedBlockInstance)
if (this.mouseoverActor !== intersectedBlockInstance.tree) {
if (this.mouseoverActor) {
this.emit(endEventName, { component: this.mouseoverActor });
}
this.mouseoverActor = intersectedBlockInstance.tree;
this.emit(startEventName, { component: intersectedBlockInstance.tree });
}
if (this.mouseoverBlockInstance !== intersectedBlockInstance) {
if (this.mouseoverBlockInstance) {
this.emit(endEventName, { component: this.mouseoverBlockInstance });
}
this.mouseoverBlockInstance = intersectedBlockInstance;
this.emit(startEventName, { component: intersectedBlockInstance, ...intersectedBlockInstanceData });
}
this.emit('blockinstancehover', intersectedBlockInstance);
}
else {
if (this.mouseoverActor !== undefined) {
this.emit(endEventName, { component: this.mouseoverActor });
this.mouseoverActor = undefined;
}
if (this.mouseoverBlockInstance !== undefined) {
this.emit(endEventName, { component: this.mouseoverBlockInstance });
this.mouseoverBlockInstance = undefined;
}
if (this.mouseoverMesh !== undefined) {
this.emit(endEventName, { component: this.mouseoverMesh });
this.mouseoverMesh = undefined;
}
}
}
/**
* Finds the block instance that is intersected closest to the perspective camera
* by a ray cast from that camera on the projected position of the last mouse move.
* Returns the intersected blockInstance's id and the intersection data.
* @returns {Object}
*/
findFirstIntersectedBlockInstance() {
const intersections = this.intersect( this.lastPointerMove.x, this.lastPointerMove.y );
//console.log( intersections )
return View.findBlockInstanceParent(intersections);
}
findFirstIntersectedConfiguration(){
const intersections = this.intersect(this.lastPointerMove.x, this.lastPointerMove.y);
return View.findConfigurationParent(intersections)
}
/**
* intersects a ray with objects in the scene and returns
* an array with intersection data objects
* @param {number} clientX - screen x-coordinate
* @param {number} clientY - screen y-coordinate
* @returns {Array<Object>}
*/
intersect = function (clientX, clientY) {
const relMx = Math.max(0, Math.min(1, (clientX - this.domElementDimensions.left) / this.domElementDimensions.width));
const relMy = Math.max(0, Math.min(1, (clientY - this.domElementDimensions.top) / this.domElementDimensions.height));
const xyVector2 = new Vector2(relMx * 2 - 1, - relMy * 2 + 1);
this.rayCaster.layers.set( Project.layerMap.visibleActors );
this.rayCaster.setFromCamera(xyVector2, this.perspectiveCamera);
return this.rayCaster.intersectObjects(this.scene.children, true);
}
/**
* Finds the first intersections with a block instance and returns that one
* along with the block instance's id
* @param {Array<Object>} intersections
* @param {('first'|'all')} find
*/
static findBlockInstanceParent(intersections, find = 'first') {
// const all = [];
for (let intersection of intersections) {
if (intersection.object.userData.PB) {
if (intersection.object.parent.userData.PB.type === 'blockInstance') {
return {
blockInstanceId: intersection.object.parent.userData.PB.origin,
blockInstance: intersection.object.parent.userData.PB.blockInstance,
intersection
};
}
else if ( intersection.object.parent.parent.userData.PB && intersection.object.parent.parent.userData.PB.type === 'blockInstance' ) {
return {
blockInstanceId: intersection.object.parent.parent.userData.PB.origin,
blockInstance: intersection.object.parent.parent.userData.PB.blockInstance,
intersection
};
}
}
}
return undefined;
}
findConfigurationParent( intersections, find ='first' ){
for (let intersection of intersections) {
if (intersection.object.userData.PB) {
if (intersection.object.parent.userData.PB.type === 'blockInstance') {
}
}
}
return undefined;
}
/**
* @param {Object} data
* @param {Vector3} data.position
* @param {HTMLElement} data.HTMLElement
* @param {BlockInstance} [data.blockInstance]
* @returns {Promise<Marker>}
*/
async addMarker({ position, HTMLElement, blockInstance }) {
const marker = new Marker({ position, HTMLElement, blockInstance, view: this });
this.markers[marker.id] = marker;
// wait until the marker has actually been placed
await new Promise(res => {
this.timer.once('update', res)
this.timer.trigger()
});
return marker;
}
/**
* @param {Configurator} configurator
* @returns {Promise<DimensionFrame>}
*/
async adddimensionFrame( configurator) {
const dimFrame = new DimensionFrame(configurator,this);
this.dimensionFrames[dimFrame.id] = dimFrame;
// wait until the marker has actually been placed
await new Promise(res => {
this.timer.once('update', res)
this.timer.trigger()
});
return dimFrame;
}
/**
* @param {Marker} marker
*/
removeMarker(marker) {
console.assert(marker instanceof Marker, 'Argument is not an instance of Marker', marker);
marker.clear();
delete this.markers[marker.id];
}
removeAllMarkers() {
Object.values(this.markers)
.forEach(this.removeMarker.bind(this));
}
expectDrag( component ){
//return new Promise()
}
rotateTo(side) {
const DEG30 = Math.PI * 0.1667;
const DEG45 = Math.PI * 0.25;
const DEG72 = Math.PI * 0.4;
const DEG90 = Math.PI * 0.5;
const DEG180 = Math.PI;
switch (side) {
case 'front':
this.cameraControls.rotateTo(0, DEG90, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 0, z: 0.5 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: 0 }, 800 ).start();
break;
case 'back':
this.cameraControls.rotateTo(DEG180, DEG90, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 0, z: - 0.5 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: 0 }, 800 ).start();
break;
case 'top':
this.cameraControls.rotateTo(0, 0, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 1, z: 0 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: 0, y: 0, z: 0 }, 800 ).start();
break;
case 'bottom':
this.cameraControls.rotateTo(0, DEG180, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: - 1, z: 0 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: 0, y: 0, z: 0 }, 800 ).start();
break;
case 'right':
this.cameraControls.rotateTo(DEG90, DEG90, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 1, y: 0, z: 0 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: DEG90 }, 800 ).start();
break;
case 'left':
this.cameraControls.rotateTo(- DEG90, DEG90, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: - 1, y: 0, z: 0 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: DEG90 }, 800 ).start();
break;
case 'front-angle':
this.cameraControls.rotateTo(0, DEG45, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 0, z: 0.5 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: 0 }, 800 ).start();
break;
case 'right-angle':
this.cameraControls.rotateTo(DEG30, DEG72, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 0, z: 0.5 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: 0 }, 800 ).start();
break;
case 'left-angle':
this.cameraControls.rotateTo(-DEG30, DEG72, true);
//new TWEEN.Tween( gridHelper.position ).to( { x: 0, y: 0, z: 0.5 }, 800 ).start();
//new TWEEN.Tween( gridHelper.rotation ).to( { x: - DEG90, y: 0, z: 0 }, 800 ).start();
break;
}
}
zoomBlock(blockID) {
}
zoomConfiguration(configurationID) {
}
getAllActors(){
const allActors = []
this.scene.traverse( function (child){
if(child.name === "Actor" ){
allActors.push(child)
// child.traverse( function (child) {
// if( child.isMesh ){
// allActors.push(child)
// }
// })
}
})
return allActors
}
zoomExtents(enableTransition = true) {
this.update()
const allActors = this.getAllActors()
const boundingBox = new Box3()
allActors.forEach(actor => boundingBox.expandByObject(actor))
const polarAngle = this.cameraControls.polarAngle
const azimutAngleDeg = THREE.MathUtils.radToDeg(this.cameraControls.azimuthAngle) % 360
const direction = () => {
if(azimutAngleDeg < 0 && azimutAngleDeg > -180 ) {
return azimutAngleDeg
}
else if (azimutAngleDeg < 0 && azimutAngleDeg < -180) {
return azimutAngleDeg + 360
}
else if (azimutAngleDeg > 0 && azimutAngleDeg < 180 ) {
return azimutAngleDeg
}
else if (azimutAngleDeg > 0 && azimutAngleDeg > 180 ) {
return azimutAngleDeg -360
}
else return 0
}
this.timer.trigger()
this.cameraControls.reset(enableTransition)
this.cameraControls.fitToBox(
boundingBox,
enableTransition,
{ paddingTop: 0.3, paddingLeft: 0.3, paddingBottom: 0.3, paddingRight: 0.3 }
)
this.cameraControls.rotateTo(THREE.MathUtils.degToRad(direction()), polarAngle, enableTransition);
}
/*
const DEG30 = Math.PI * 0.1667;
const DEG45 = Math.PI * 0.25;
const DEG72 = Math.PI * 0.4;
const DEG90 = Math.PI * 0.5; aka THREE.MathUtils.degToRad(90)
const DEG180 = Math.PI;
*/
zoomScene() {
this.update()
const allActors = this.getAllActors()
const boundingBox = new Box3()
//create boundingBox for all meshes in all actors
for( var i = 0; i < allActors.length; i++ ){
boundingBox.expandByObject( allActors[i] )
}
// make a BoxBufferGeometry of the same size as Box3
const dimensions = new THREE.Vector3().subVectors( boundingBox.max, boundingBox.min );
const boxGeo = new THREE.BoxBufferGeometry(dimensions.x, dimensions.y, dimensions.z);
// make a mesh
const mesh = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial( { color: 0xffcc55 } ));
this.timer.trigger()
// this.cameraControls.reset(true)
// this.rotateTo('left-angle');
this.cameraControls.fitToSphere(
mesh, //hier kun je een box3.boundingSphere of een mesh gebruiken.
true
)
}
zoomTop() {
this.update()
const allActors = this.getAllActors()
const boundingBox = new Box3()
//create boundingBox for all meshes in all actors
for( var i = 0; i < allActors.length; i++ ){
boundingBox.expandByObject( allActors[i] )
}
//const boundingBox = new Box3().setFromObject(this.scene.getObjectByName("Actor"))
this.timer.trigger()
//this.cameraControls.normalizeRotations()
this.cameraControls.reset(true)
this.rotateTo('top');
this.cameraControls.rotateTo(0, 0, true);
this.cameraControls.fitToBox(
boundingBox,
true,
{ paddingTop: 0.3, paddingLeft: 0.3, paddingBottom: 0.3, paddingRight: 0.3 }
)
}
toggleBoundingBoxes() {
this.perspectiveCamera.layers.toggle(2);
this.update();
}
showBoundingBoxes( configuration ){
//console.log( configuration )
this.scene.traverse(function(child) {
if ( child instanceof Box3Helper ) {
//console.log( child)
child.visible = false
}
if ( child.name === "Vertex Helper" ) {
//console.log( child)
child.visible = false
}
})
for( let child of configuration.content.main.medium.children ){
if( child instanceof Box3Helper ) {
child.visible = true
}
}
this.perspectiveCamera.layers.enable( 2 );
this.update();
}
hideBoundingBoxes(){
this.perspectiveCamera.layers.disable( 2 );
this.update();
}
toggleDimensions() {
let visibility
this.perspectiveCamera.layers.toggle(3);
this.scene.traverse(function (child) {
// if (child instanceof Group && child.name === "dimLine") {
// if (child.visible === false) { child.visible = true; }
// else { child.visible = false; }
// };
if (child instanceof CSS2DObject && child.name === "dimLabel") {
if (child.visible === false) {
child.visible = true;
visibility = true
} else {
child.visible = false;
visibility = false
}
};
});
this.update();
return visibility
}
dimensionsVisible = false;
showDimensions() {
this.perspectiveCamera.layers.enable(3); // enable dimensions
this.scene.traverse(function (child) {
if (child instanceof CSS2DObject && child.name === "dimLabel") {
if (child.visible === false) {
child.visible = true;
}
};
});
this.dimensionsVisible = true;
this.update();
}
hideDimensions() {
this.perspectiveCamera.layers.disable(3); // enable dimensions
this.scene.traverse(function (child) {
if (child instanceof CSS2DObject && child.name === "dimLabel") {
if (child.visible === true) {
child.visible = false;
}
};
});
this.dimensionsVisible = false;
this.update();
}
// clearDimensions(){
// this.scene.traverse(function (child) {
// if (child instanceof CSS2DObject && child.name === "dimLabel") {
// child.remove()
// }
// });
// }
project(point, cam) {
cam = cam !== undefined ? cam : this.perspectiveCamera;
console.assert(point instanceof Vector3, 'Position should be a THREE.Vector3');
console.assert(cam instanceof Camera, 'Camera should be a THREE.Camera instance');
const v = point.project(this.perspectiveCamera);
const coords = {
x: (v.x + 1) / 2 * this.domElementDimensions.width,
y: - (v.y - 1) / 2 * this.domElementDimensions.height
};
return coords;
}
}
export { View }