Source: view/view.js

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 }