Source: project.js

import { Package } from './package/package.js';
import { Timer } from './timer.js';
import { Reporter } from './reporter/reporter.js';
import { Actor } from './actor/actor.js';
import { View } from './view/view.js';
import { Block } from './package/block/block.js';
import { DefaultView } from './view/default_view.js';
import { InformationSource } from './reporter/information_source.js';
import { Configurator } from './configurator/configurator.js';
import { LoadingBase } from './package/loader/loading_base.js';
import { BlockInstance } from './configurator/block_instance.js';
import { ComponentLoader } from './package/loader/component_loader.js';
import { Scene, WebGLRenderer, Quaternion, Euler, Vector3 } from '../node_modules/three/build/three.module.js';
import { makeImage } from './lib.js';
import defaultRendererSettings from './default_settings/renderer.js';
import { SingleBlockInstance } from './actor/single_block_instance.js';
import { ServerConnection } from './server/server_connection.js';
import { StateTracker } from './state_tracker.js';
import { Transform } from './transform/transform.js';
import { pick } from "./lib.js"
import { Component } from './component/component.js';
import { Theme } from './package/theme/theme.js';
import { Configuration } from './configurator/configuration.js';

// App has 1 Scene
// App has n View
// App has n Packages
// App has n Configurator

// Configurator = Package => Config => Object3D




// App moet projecten inladen. 

// Er moet toch iets van een base.productbuilder.com komen, die aan loaders kan vertellen welke packages en materiallibs waar staan.
// Wellicht zou dit met git hooks kunnen werken: package is een repo, repo ergens planten -> meldt zich aan bij packagebase.


/**
 * Base class 
 * @constructor
 * 
 * @mermaid
 *   graph TD;
 *     ComponentTree-->Package;
 *     ComponentTree-->Configuration;
 *     GeometryFile-->Geometry;
 *     Geometry-->Mesh;
 *     WrappedImage-->WrappedTexture;
 *     WrappedTexture-->WrappedMaterial;
 *     WrappedMaterial-->MaterialCategory;
 *     WrappedMaterial-->MaterialSet;
 *     MaterialCategory-->MaterialSet;
 *     WrappedMaterial-->Mesh;
 *     MaterialSet-->Mesh;
 *     Mesh-->MeshGroup;
 *     Mesh-->PositionedMesh;
 *     MeshGroup-->PositionedMeshGroup;
 *     PositionedMesh-->Block;
 *     PositionedMeshGroup-->Block;
 *     ConnectorType-->Connector;
 *     Connector-->Block;
 *     Block-->Package;
 *     Connector-->Configuration;
 *     Block-->Configuration;
 *     Package-->Configuration;
 *     Configuration-->Project;
 */

/**
 * @class Project
 * An actor should not be removed during runtime, or undo/redo will mess up
 */
class Project extends StateTracker {

    /**
     * @param {Reporter} reporter
     * @param {Object} settings
     * @param {URL} [settings.server]
     * @param {Object} [settings.rendererSettings]
     * @param {number} [settings.reconnectTime = 5]
     */

    constructor(reporter, settings = {}) {

        super(reporter, settings);

        window.onerror = (message, source, lineno, colno, error) => {
            this.report({
                msg: 'Uncaught error: ' + message,
                level: 'error'
            });
            console.error(error);
        }

        Object.defineProperty(window, 'uuid', {
            get: InformationSource.uuid
        });

        const project = this;


        this.exportConfig = function () {

            var project = this;
            var config;

            window.exportConfig = function () {

                config = JSON.stringify(pick(
                    ['info', 'blockInstances', 'connections', 'materialAssignments', 'themes'],
                    project.configurators[0].configuration.toJSON()
                )
                )
                console.log(config)

            }

        }

        this.exportConfig()


        this.rendererSettings = {
            ...defaultRendererSettings,
            ...(settings.rendererSettings || {})
        };

        //default renderer for the perspective camera
        this.renderer = new WebGLRenderer(this.rendererSettings);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setClearColor(0x000000, 0);

        //renderer for the orthographic camera
        this.rendererOrtho = new WebGLRenderer(this.rendererSettings);
        this.rendererOrtho.setPixelRatio(window.devicePixelRatio);

        Project.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy()

        this.timer = new Timer(this._reporter);

        this.timer.on(
            'update',
            async () => {

                if (this.bodyChangeHandlePromise === null) {
                    // console.log('update views')
                    for (let view of this.views) {
                        view.update();
                    }
                }
                else {
                    // for (let transform of Object.values(this.transforms)) {
                    //     transform.relinkObject3Ds();
                    // }

                    for (let view of this.views) {

                        const projectData = this.configurators.map(configurator => ({
                            boundingBox: configurator.configuration.boundingBox,
                            configurationId: configurator.configuration.id,
                            id: configurator.id
                        }));

                        view.hideDimensions()

                        view.onSceneUpdate(projectData); // also updates view
                    }

                    let stateChange = false;
                    let newState = {};

                    for (let actor of this.actors) {

                        if (actor.visible === false) {
                            continue;
                        }

                        newState[actor.slug] = actor.cursor;

                        if (this.state?.[actor.slug] === undefined || this.state[actor.slug] !== newState[actor.slug]) {
                            // console.log('state change', actor.slug, this.state?.[actor.slug], '=>', newState[actor.slug]);
                            stateChange = true;
                        }
                    }

                    if (stateChange) {

                        // console.log('Adding new state')

                        await this.addState({
                            newState,
                            moveCursor: 'silent'
                        });

                        // console.log(this._stateRegister);

                        // console.log('Added new state')
                    }

                    this.bodyChangeHandlePromiseResolver();
                    this.bodyChangeHandlePromise = null;
                    this.bodyChangeHandlePromiseResolver = null;
                }
            }
        );

        if (settings.server) {
            this.server = new ServerConnection(
                reporter,
                {
                    url: settings.server || new URL('wss://backend.productbuilder.nl'),
                    reconnectTime: settings.reconnectTime || 5
                }
            );
        }

        // this.addTransformClass(DefaultSelectionTransform);
        // this.addTransformClass(ColorTransform);

        // StateTracker must have an initial state

        // this._stateRegister = [
        //     // {}
        // ];

        const autosaveData = this.loadFromLocalStorage('autosave');

        if (autosaveData) {
            this.autosave = {
                age: ( new Date().getTime() - autosaveData.meta.time ) / 1000,
                data: autosaveData,
                restore: async () => {
                    console.log('restoring autosave');
                    return project.buildProjectFromExport(autosaveData);
                }
            };
            // console.log('autosave found', autosaveData.meta.time);
        }

        this.reset();
    }


    /**
     * Map of 3D content type to THREE layer to keep everything nicely (and consistently) separated
     * @type {Object.<string,number>}
     */

    static layerMap = {
        grid: 1,
        boundingBoxes: 2,
        dimLines: 3,
        connectorHelpers: 4,
        lightHelpers: 5,
        visibleActors: 6,
        hiddenActors: 7
    };

    static maxAnisotropy

    /**
     * Set of quaternions that are used often
     * @type {Object<string,Quaternion>}
     */

    static standardQuaternions = {
        '+x': new Quaternion().setFromEuler(new Euler(Math.PI, 0, 0)),
        '-x': new Quaternion().setFromEuler(new Euler(-Math.PI, 0, 0)),
        '+y': new Quaternion().setFromEuler(new Euler(0, Math.PI, 0)),
        '-y': new Quaternion().setFromEuler(new Euler(0, - Math.PI, 0)),
        '+z': new Quaternion().setFromEuler(new Euler(0, 0, Math.PI)),
        '-z': new Quaternion().setFromEuler(new Euler(0, 0, -Math.PI)),
    };


    /**
     * @type {Object<string,HTMLImageElement>}
     */

    static defaultImages = {
        missing: makeImage(128, 128, 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAEzklEQVR4nO3dsW7bSBCAYRVJe5DTOEiRGK4CuNEj6BHYEJxZN2pOCFKm8iPcA6W4IkHUB4HdnZtAblLHb5Brdn3rxeYkWTtLkfo/wI2L3SVnSJGzS3IyAQAAAAAAAAAAAAAAAAAAAAAAAEoSkYWqXjdNM+17LE/VNM1UVa9FZNH3WAbFB/+X/xtkEoTgh+0gCbaUBH+QSZAGnyTYUib4d0NLgkzw70iCLYjIWbqjmqaZishN9P9V3+PcRFVX0Tbc+G1YJNt21vc4D1LYUfFREpJARO6dc7Meh7cV59xMRO5D8MP/c9uGjNzR0TTNdAjBD5xzs9zPFUc+AHAqzDiafWJxMdR13QdVPS3V3ib+juV9wfaO4wLR4nZIVa98e7c1kkBEzkRk7cf/rkR7R1EnyAR/sW+by+XyeVJ0+ccyCeLg+234Op/PnxVot/i+OSiWG+icO0mS4NY596ZU+0Em+N/atn1RsP3xJkFczRORdemSbiYJ7komgXXwJ5OHglfcx03J9nuVKekWr+tbJUHbti9rBD+ZObwZwrzHTmongYj8cM6d79umiLxS1e/hGoPg7yFOAqvavnPuREQ+qepFwTbPVfVz27YvS7UZtT0TkfvRBz/wGb8aUm3fmnNupqqr0QcfAIANLH8Lc6uNa6ze5fd9SyLyl1VdX1Wb9DY0s4avKd1vVEi6Kt32qIjIn3FJt/RtV64WYX0PnlYRu65rS7Y/Kqp66o/+hyQofSbIJIFZ8GtUEUenjySwCr6fnXyYqST4W/JJcF0jCSoGv3gVcdQykzt/l+4jXASWbldEPlpeyxyNkASlJnZqcc6di8gPjvwCfBIUm9ipRVXfEnwAAOyoatNHnd2XkIuXjbGDaCVt1XcHxPMHo1q9OzTWawxzcmv4rPvEb9RYaJr2dzQLOIeiVhIQ/ANVKzC1zzbYQu2jkiQ4MNZTujm5JLDuE78RPVdf9fc4ecBlUatfZFAIAgBgnPz6urd9j2NXqnrhnDvpexyDFhZXDnhJ2DVJ8EQ++LfRff7H0n1YLQrV6IXRJMET1FhWHVURLd5U8iZ5toEk2Fbl4JuVdDMPuJAEm7Rt+6KH4JMEh6Lrujb6zV+3bfu6ZPu5ySPryZ3MtcyiZPujo6pXIrK2eHmyRo+Hh/mDzORO8fJudCbg8fBtWL8gIp08ip4TXFj1e3l5+YdV2wAADIH/vf3CiyL/498W+mX0y8uS27CfFkngb7s+l5w7UNULEflkcQ/v3xL6c/RrDGss4EyqiN9F5NW+bUYTOyaFnKNYaFop+I+qiL6QtHcV0df1H33CliTYUbJxdxbBF5FvcfBLFpJqJUHaR8n2e2X5ORTr4AfWSTDqT8ZMJjYbOJ/Pn4nIV+vgB7kkWC6Xz/dtd/TBDyw+Gyci72oEP0jm+veu7R/NZ+MCMfhIoqq+rxH8qL/Trus+lGrPYp8ctJrBGgr2CQBkT4VN00yHNHfgnJvlah2c5jfIXQxFVUSTuYPSotr+o2re0V3o7Sp3O5RZwLnqe5ybaPJMgC/zFr/9HaXMjoo/tjCId/JkvkWwThO77zEetDQJhhT84H++SrLoe2yDECfB0IIfZM4Ei77HNCi51btDU2O1MQAAAAAAAAAAAAAAAAAAAAAAAI7UvxmoXFiIe/GjAAAAAElFTkSuQmCC')
    };


    /**
     * @type {string}
     */

    static basis = '+X+Y+Z';



    /** @type {(Promise|null)} */

    bodyChangeHandlePromise = null;
    bodyChangeHandlePromiseResolver = null;


    /** @type {Timer} */

    timer;


    /**
     * @type {Scene}
     */

    scene;


    /**
     * @type {WebGLRenderer}
     */

    renderer;


    /**
     * @type {ServerConnection}
     */

    server;




    /** 
     * List of packages
     * @type {Array<Package>} 
     */

    pkgs;

    /**
     * @param {URL} packageURL
     */

    async addPackage(packageURL) {

        console.assert(packageURL instanceof URL);

        const indexURL = `${packageURL.href}${packageURL.href.substr(-1) !== '/' ? '/' : ''}index.json?r=${Math.random()}`;

        this.report({ msg: `Loading index from ${indexURL}` });

        const response = await fetch(indexURL);

        //console.log( response )

        if (!response.ok) {
            console.warn(response);
            throw new Error('Load package failed');
        }

        const packageJSON = await response.json();

        // console.log( packageJSON  )

        const packageURLBase = new LoadingBase(
            this._reporter,
            { name: 'Package URL base', url: packageURL }
        );

        const loader = new ComponentLoader(this._reporter, { name: 'PackageURLLoader', bases: [packageURLBase] });

        const pkg = await Package.createFromJSON(
            packageJSON,
            this._reporter,
            null,
            {
                loader,
                renderer: this.renderer
            });

        Object.freeze(pkg)

        // const pkg = new Package(
        //     this._reporter,
        //     {},
        //     {
        //         loader,
        //         renderer: this.renderer
        //     }
        // );

        // await pkg.importJSON(packageJSON);

        this._addPackageObject(pkg);

        this.report({ msg: `Loaded ${pkg.label}`, level: 'notice' });

        return pkg;
    }

    /**
     * Add a new package to the internal register
     * @param {Package} pkg
     */

    _addPackageObject(pkg) {
        this.report({ msg: `Adding ${pkg.label}`, level: 'notice' });
        this.pkgs.push(pkg);
    }



    get actors() {
        return [...this.configurators, ...this.singleBlockInstances];
    }

    /**
     * @param {Actor} actor
     */

    addActor(actor, list, overwritePreviousState = false) {
        console.assert(actor instanceof Actor);

        this.report({ msg: `Adding ${actor.label}`, level: 'notice' });
        list.push(actor);
        this.scene.add(actor.body);

        actor.on('bodychange', async (amendPreviousState = false) => {

            // console.log(`${actor.label} body change`);

            if (this.bodyChangeHandlePromise === null) {
                let resolve = null;
                this.bodyChangeHandlePromise = new Promise(res => this.bodyChangeHandlePromiseResolver = res);
                this.timer.trigger();
                // this.bodyChangeHandlePromiseResolver.resolve = resolve;
            }
            await this.bodyChangeHandlePromise;
            // console.log('resolved');


            if (overwritePreviousState === true) {

                // the bodychange event will trigger a state addition


                // console.log('execute squash')
                this.overwritePreviousState();
                overwritePreviousState = false; // only do this the first time

            }
            this.timer.trigger();
        });

        this.timer.trigger();
    }



    /** 
     * List of sbi's
     * @type {Array<SingleBlockInstance>}
     */

    singleBlockInstances = [];

    /**
     * @param {Block} block
     * @param {Vector3} position
     * @param {Quaternion} quaternion
     * @returns {SingleBlockInstance}
     */

    addSingleBlockInstance(block, position, quaternion) {

        if (this.pkgs.indexOf(block.tree) === -1) {
            throw new Error('Block belongs to unknown tree');
        }

        const blockInstance = new BlockInstance(this._reporter, { block });

        const SBI = new SingleBlockInstance(
            this._reporter,
            {
                blockInstance: blockInstance,
                loader: block.tree.loader,
                position: position || new Vector3(),
                quaternion: quaternion || new Quaternion()
            }
        );

        this.addActor(SBI, this.singleBlockInstances);

        return SBI;
    }



    /** 
     * List of configurators
     * @type {Array<Configurator>} 
     */

    configurators;

    /**
     * @param {Object} settings
     * @param {Package} settings.pkg
     * @param {Boolean} [overwritePreviousState=false]
     * @returns {Promise<Configurator>}
     */

    async addConfigurator(settings, overwritePreviousState = false) {

        if (this.pkgs.indexOf(settings.pkg) === -1) {
            throw new Error('Unknown package');
        }

        // console.log('squash=', overwritePreviousState)

        const configurator = new Configurator(this._reporter, settings);

        this.addActor(configurator, this.configurators, overwritePreviousState);

        return configurator;
    }





    // /** 
    //  * List of transforms
    //  * @type {Object<string,Transform>} 
    //  */

    // transforms = {};

    // /**
    //  * @param {typeof Transform} transformClass
    //  * @returns {Transform}
    //  */

    // addTransformClass(transformClass) {
    //     this.transforms[transformClass.name] = new transformClass(
    //         this._reporter,
    //         {
    //             timer: this.timer,
    //             scene: this.scene,
    //         }
    //     );

    //     return this.transforms[transformClass.name];
    // }





    findConfigurator(configuration) {
        return this.configurators.find(c => c.configuration === configuration);
    }





    /** 
     * List of views
     * @type {Array<View>} 
     */

    views = [];

    /**
     * @param {View} [view]
     */

    addView(view) {

        if (!view) {
            view = new DefaultView(
                this._reporter,
                {
                    scene: this.scene,
                    renderer: this.renderer,
                    rendererOrtho: this.rendererOrtho,
                    timer: this.timer,
                    findConfigurator: this.findConfigurator.bind(this),
                    addConfigurator: this.addConfigurator.bind(this),
                    findComponentById: this.findComponentById.bind(this)
                }
            );
        }

        this.views.push(view);


        view.onSceneUpdate();

        return view;
    }


    // the cursor was moved



    /** @param {any} selectedState */

    onStateCursorMoved(selectedState) {

        // console.log('state cursor moved, selected state:', selectedState)
        // console.log(this._stateRegister, this._cursor)

        for (let actor of this.actors) {
            if (selectedState[actor.slug] === undefined) {
                if (actor.visible === true) {
                    console.log('hide', actor.label)
                    actor.hide();
                }
            }
        }

        for (let actor of this.actors) {
            if (selectedState[actor.slug] !== undefined) {
                if (actor.visible !== true) {
                    console.log('show', actor.label)
                    actor.show();
                }
                actor.setCursor(selectedState[actor.slug]);
            }
        }
    }


    /**
     * @param {number} [steps =1]
     * @returns {Promise<Object>}
     */
    undo(steps = 1) {
        // this.disableTransforms();
        for (let view of this.views) {
            view.removeAllMarkers();
        }
        return super.undo(steps);
    }


    /**
     * @param {number} [steps =1]
     * @returns {Promise<Object>}
     */

    redo(steps = 1) {
        // this.disableTransforms();
        for (let view of this.views) {
            view.removeAllMarkers();
        }
        return super.redo(steps);
    }


    removeConfigurator(configurator) {

        const index = this.configurators.indexOf(configurator);

        if (index === -1) {
            throw new Error('Unknown configurator');
        }

        if (this.scene) {
            this.scene.remove(configurator.body);
        }

        configurator.removeAllListeners();
        configurator.clearBody();
        configurator.resetState();

        this.configurators.splice(index, 1);
    }

    // disableTransforms() {
    //     for (let transform of Object.values(this.transforms)) {
    //         transform.removeAll(true);
    //     }
    // }


    reset(removePkgs = false) {
        this.resetState();
        this.removeAllListeners();

        if (this.configurators) {
            for (let configurator of this.configurators) {
                this.removeConfigurator(configurator);
            }
        }

        this.configurators = [];

        // pkgs weggooien is niet nodig
        if (this.pkgs && removePkgs === true) {
            for (let pkg of this.pkgs) {
                pkg.removeAllListeners();
            }
        }

        if (!this.pkgs || removePkgs === true) {
            this.pkgs = [];
        }



        // clear scene 

        if (!this.scene) {
            this.scene = new Scene();
        }
        // if ( this.scene ) {
        //     while (this.scene.children.length > 0) {
        //         this.scene.remove(this.scene.children[0]);
        //     }
        // }

        // this.scene = new Scene();


        // remove any old update requests

        for (let urId of Object.keys(this.timer.updateRequests)) {
            this.timer.removeUpdateRequest(urId);
        }


        // create bounding box for the entire project

        this.boundingBox = null;
    }


    /**
     * @method
     * @param {UUID|String} identifier Project id or slug
     */

    async load(identifier) {

        if ((!this.server) || (!this.server.connected)) {
            throw new Error('Unable to load project, not connected to server.');
        }

        let projectData = null;

        try {
            const response = await this.server.request({
                endpoint: 'project',
                method: 'read',
                data: {
                    identifier
                }
            });

            projectData = JSON.parse(response.data.export);

            this.report({ msg: `Project data load success`, level: 'notice' });
        }
        catch (err) {
            console.error('Error while loading project:', err);
        }


        // console.log(projectData);

        return this.buildProjectFromExport(projectData);
    }

    loadFromLocalStorage(key) {
        let projectData = null;
        if (localStorage) {
            const projectDataStr = localStorage.getItem(key);
            if (projectDataStr) {
                projectData = JSON.parse(projectDataStr);
            }
        }
        return projectData;
    }



    async buildProjectFromExport(projectData) {

        this.reset();

        for (let pkgToLoad of projectData.packages) {
            const loadedPkg = this.pkgs.find(pkg => pkg.id === pkgToLoad.id);
            if (loadedPkg) {
                console.log(loadedPkg.label, 'already loaded')
            }
            else {
                await this.addPackage(new URL(pkgToLoad.url));
            }
        }

        const configurators = await Promise.all( projectData.configurations.map(async (configuratorData)=>{
            const pkg = this.pkgs.find(pkg => pkg.id === configuratorData.configuration.pkg);
            // console.log(pkg, this.pkgs, configuratorData)
            const configuration = Configuration.createFromJSON(configuratorData.configuration, this._reporter, pkg);
            const configCopy = configuration.copy()

            const configurator = await this.addConfigurator({
                pkg,
                position: new Vector3(configuratorData.position.x, configuratorData.position.y, configuratorData.position.z),
                quaternion: new Quaternion(configuratorData.quaternion.x, configuratorData.quaternion.y, configuratorData.quaternion.z, configuratorData.quaternion.w),
                configuration: configCopy
            });
            
            // console.log('update configurator with config copy');
            await configurator.update(configCopy)

            // console.log('updated', configurator.cursor, configurator._stateRegister);
            // console.log(configCopy);

            // position: new Vector3( configuratorData.position.x, configuratorData.position.y,  configuratorData.position.z ),
            //     quaternion: new Quaternion(  configuratorData.quaternion._x,configuratorData.quaternion._y,configuratorData.quaternion._z,configuratorData.quaternion._w)
            this.timer.trigger();
            return configurator
        }))
        return configurators
    }
    

    async save({ copy = false, metadata = undefined } = {}) {

        // wait for configs to build, otherwise assignables array can still be
        // empty in export (and perhaps other issues too)
        await Promise.all(this.configurators.map(c => c.configuration.build()));

        const exp = {
            meta: {
                time: new Date().getTime()
            },
            packages: this.pkgs.map(pkg => ({
                id: pkg.id,
                url: pkg.loader.loadingBases[0].url.href
            })),
            configurations: this.configurators.map(
                configurator =>
                ({
                    configuration: configurator.configuration.toJSON(),
                    position: configurator.state.position,
                    quaternion: configurator.state.quaternion
                })
            )
            // singleBlockInstances: this.singleBlockInstances.map(SBI =>
            // ({
            //     sbi: SBI.toJSON(),
            //     position: SBI.state.position,
            //     quaternion: SBI.state.quaternion
            // })
            // )
        }

        // console.log(exp)

        const expStr = JSON.stringify(exp);

        if (localStorage) {
            // console.log('autosave');
            localStorage.setItem('autosave', expStr);
        }

        let returnObject = { export: exp };

        if ((!this.server) || (!this.server.connected)) {
            throw new Error('Unable to save project to server, not connected.');
        }
        else {

            try {
                const saveResponse = await this.server.request({
                    endpoint: 'project',
                    method: copy === true ? 'create' : 'update',
                    data: {
                        id: this.id,
                        name: '',
                        description: '',
                        export: expStr,
                        metadata: metadata ? JSON.stringify( metadata ) : undefined
                    }
                });

                // console.log('saveResponse', saveResponse)

                if (saveResponse.data) {
                    this.report({ msg: 'Project saved', level: 'notice' });
                    this.slug = returnObject.slug = saveResponse.data.slug;
                    this.price = returnObject.price = saveResponse.data.price;
                }
                else {
                    this.report({ msg: 'Project could not be saved: ' + saveResponse.errors[0]?.description, level: 'error' });
                    this.price = undefined;
                }

            }
            catch (err) {
                console.error('Error while saving project:', err);
            }
        }

        return returnObject;
    }



    /**
     * Search the loaded packages and current confugurations for a component
     * with the supplied id
     * @param {UUID} id 
     * @returns {Component}
     */

    findComponentById(id) {
        let component = undefined;
        for (let pkg of this.pkgs) {
            component = pkg.findComponentById(id);
            if (component) {
                break;
            }
        }
        if (component === undefined) {
            for (let configurator of this.configurators) {

                component = configurator.configuration.findComponentById(id);

                if (component) {
                    break;
                }
            }
        }

        return component;
    }


    /**
     * Sets a theme accross all configurators for the relevant package
     * and squashes their state updates into one
     * @param {Theme} theme
     */

    setTheme(theme) {
        for (let i = 0, l = this.configurators.length; i < l; i += 1) {
            this.configurators[i].setTheme(theme);
            if (i > 0) {
                this.overwritePreviousState();
            }
        }
    }
}

export { Project as Project };