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, 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 { EffectComposer } from '../../node_modules/three/examples/jsm/postprocessing/EffectComposer.js';
import { Pass } from '../../node_modules/three/examples/jsm/postprocessing/Pass.js';
import { RenderPass } from '../../node_modules/three/examples/jsm/postprocessing/RenderPass.js';
import { OutputPass } from '../../node_modules/three/examples/jsm/postprocessing/OutputPass.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 defaultRendererSettings from '../default_settings/renderer.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;
                }
            }
        );

        //console.log(settings)

        const view = this;



        this.markers = {};
        this.dimensionFrames = {};

        this.domElementDimensions;

        this.camTween = null;

        this.tweens = {};

        this.scene = settings.scene;

        if(!settings.renderer){
            console.warn("no render settings have been specified, reverting to default render settings")
            if(settings.renderSettings){
                this.renderer = new THREE.WebGLRenderer(settings.renderSettings)
            }else{
                this.renderer = project.renderer //new THREE.WebGLRenderer(defaultRendererSettings) 
            }
        }else{
            this.renderer = settings.renderer;
        }

        //this.renderer.setPixelRatio(window.devicePixelRatio);
		//this.renderer.setSize( container.offsetWidth, container.offsetHeight );

        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.shadowPlane );
        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 = 0;

        this.outputPass = new OutputPass();

        // console.log( this.renderPass )
        // console.log( this.outputPass )

        this.passes = [
            this.renderPass,         // deze pass moet als eerste komen!
            this.outputPass,
            ...(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);

        // limit lowest camera angle
        const maxPolarAngle = this.cameraSettings?.maxPolarAngle ?? 180;
        this.cameraControls.maxPolarAngle = THREE.MathUtils.degToRad(maxPolarAngle)
        

        this.cameraControls.normalizeRotations()

        this.cameraControls.addEventListener('control', evt => {
            // console.log('control')
            this.timer.addUpdateRequest(this.id, 'Camera controls control');
        })

        this.cameraControls.addEventListener('controlstart', evt => {
            // console.log('controlstart')
            this.timer.addUpdateRequest(this.id, 'Camera controls start');
        })
        this.cameraControls.addEventListener('wake', evt => {
            // console.log('wake')
            this.cameraControlsAwake = true;
            this.timer.addUpdateRequest(this.id, 'Camera controls wake');
        })
        this.cameraControls.addEventListener('sleep', evt => {
            // console.log('sleep')
            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.domElement.addEventListener(
        //     'touchstart',
        //     this.onTouchStart.bind(this),
        //     {passive: true}
        // );

        // this.domElement.addEventListener(
        //     'touchmove',
        //     this.onTouchMove.bind(this),
        //     {passive: true}
        // );

        // this.domElement.addEventListener(
        //     'touchend',
        //     this.onTouchEnd.bind(this),
        //     {passive: true}
        // );

        // this.domElement.addEventListener(
        //     'touchcancel',
        //     this.onTouchCancel.bind(this),
        //     {passive: true}
        // );


        

        //Click Event Listeners
        this.domElement.addEventListener(
            'click',
            this.onClick.bind(this)
        );

        // this.domElement.addEventListener(
        //     'dblclick',
        //     this.onDoubleClick.bind(this)
        // );

        // this.domElement.addEventListener(
        //     'fastclick',
        //     event => { console.log('fast click') }
        // );


        //Pointer Events Listeners
        document.addEventListener(
            'pointerup',
            this.onPointerUp.bind(this)
        );

        document.addEventListener(
            'pointerdown',
            this.onPointerDown.bind(this)
        );
        
        document.addEventListener(
            'pointermove',
            this.onPointerMove.bind(this)
        );



        //Drag Event Listeners (Default)
        this.domElement.addEventListener(
            'dragenter',
            this.onDragEnter.bind(this)
        );

        this.domElement.addEventListener(
            'dragover',
            this.onDragOver.bind(this)
        );

        document.addEventListener(
            'drop',
            this.onDrop.bind(this)
        );

        document.addEventListener(
            'dragleave',
            this.onDragLeave.bind(this)
        );

        document.addEventListener(
            'dragend',
            this.onDragEnd.bind(this)
        );


        //Drag Event Listeners (PB)
        document.addEventListener(
            'componentdragenter',
            this.onComponentDragEnter.bind(this)
        );

        document.addEventListener(
            'componentdragleave',
            this.onComponentDragLeave.bind(this)
        );
                        

        //Window Event Listeners
        window.addEventListener(
            'resize',
            () => { setTimeout( this.updateDOMElementDimensions.bind(this), 1) } // Needs a tick to be triggered after resize from portrait to landscape on mobile 
        );

        
        //Keyboard Event Listeners
        document.addEventListener( 
            'keyup',
            this.onKeyUp.bind(this)

        );



        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)
     */

    updateDOMElementDimensions() {

        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) {
            // console.log( pass )
            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.renderer.render(this.scene, this.perspectiveCamera);
        this.composer.render(this.scene, this.perspectiveCamera);
        this.css2dRenderer.render(this.scene, this.perspectiveCamera);
    }

    renderOrtho() {
        this.composer.renderer.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.bind(this, 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 = [];

        

        //loop through array with intersection
        for (let intersection of intersections) {


            //check userData
            if ( intersection.object.userData.PB ) {

                

                //check if it is a blockInsatnce
                if (intersection.object.parent.userData.PB.type === 'blockInstance') {

                    //check if block is on visible layer
                    //if( intersection.object.parent.userData.PB.blockInstance._content.main.medium.layers.mask === 64 ){
                        
                        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'  ) {

                    //check if block is on visible layer
                    //if( intersection.object.parent.parent.userData.PB.blockInstance._content.main.medium.layers.mask === 64 || 1 ){

                        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 {Vector3} position
     * @param {HTMLElement} HTMLElement
     * @param {BlockInstance} [blockInstance]
     * @param {string} [name]
     * @returns {Promise<Marker>}
     */

    async addMarker({ position, HTMLElement, blockInstance, name }) {

        const marker = new Marker({ position, HTMLElement, blockInstance, view: this, name });
        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()

        for( const configurator of project.configurators ){
            if( configurator.visible === true ){
                boundingBox.expandByObject( configurator.body )
            }
        }

        //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.BoxGeometry(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() {

        console.log("engine//view.toggleBoundingBoxes()")
        // this.perspectiveCamera.layers.toggle(2);
        // this.update();

    }

    showBoundingBoxes( configuration ){

        console.log("showBoundingBoxes()" )


        //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(){
        console.log("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

        for( const configurator of project.configurators ){

            if( configurator.visible ){

                configurator.body.traverse(function (child) {

                    if (child instanceof CSS2DObject && child.name === "dimLabel") {
                        if (child.visible === false) { 
                            child.visible = true; 
                        }
                    }
                    if( child.name === "dimLine" ){
                        if (child.visible === false) { 
                            child.visible = true; 
                        }
                    }

                })

            }
        }

        // 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) {
            //the dimLabel is a marker that is add in the scene
            if (child instanceof CSS2DObject && child.name === "dimLabel") {
                if (child.visible === true) { 
                    child.visible = false; 
                }
            };
            if( child.name === "dimLine" ){
                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 }