Source: configurator/configuration.js

import { omit, checkPropTypes, unique, getAssigningComponent, pick } from '../lib.js';
import { Project } from '../project.js'
import { Block } from '../package/block/block.js';
import { Group, Material, Object3D, Vector3, Quaternion, Euler, Math, BoxGeometry, BufferGeometry, Texture, BufferAttribute, MeshBasicMaterial, Mesh, Box3, Box3Helper, Matrix4 } from '../../node_modules/three/build/three.module.js';
import { Connector } from '../package/connector/connector.js';
import { ConnectorType } from '../package/connector/connector_type.js';
import { Reporter } from '../reporter/reporter.js';
import { Package } from '../package/package.js';
import { BlockInstance } from './block_instance.js';
import { ComponentTree } from '../component/component_tree.js';
import { Component } from '../component/component.js';
import { Connectable } from './connectable.js';
import { Connection } from './connection.js';
import { MaterialAssignment } from './material_assignment.js';
import { WrappedMesh } from '../package/mesh/wrapped_mesh.js';
import { MaterialSet } from '../package/material/material_set.js';
import { calculatePlacement } from '../utilities/calculatePlacement.js';
import findConnectedConnectors from '../utilities/findConnectedConnectors.js';
import { findExtendOptions, findInsertOptions, findBlockInstanceOptions, findReplaceOptions } from '../utilities/findConfigOptions.js';
import { WrappedMaterial } from '../package/material/wrapped_material.js';
import { PositionedComponent } from '../package/component/positioned_component.js';

import { getDimensions } from '../utilities/getDimensions.js';
import { drawDimensionsLine } from '../utilities/drawDimensionsLine.js';
import { Theme } from '../package/theme/theme.js';

import { v4 as uuid } from '../../node_modules/uuid/dist/esm-browser/index.js';
import { MeshStandardNodeMaterial } from '../../node_modules/three/examples/jsm/nodes/Nodes.js';

/**
 * @typedef {Object} Instruction
 * @property {Object} [remove]
 * @property {BlockInstance} [remove.blockInstance]
 * @property {Array<BlockInstance>} [remove.blockInstanceChildren]
 * @property {Array<MaterialAssignment>} [remove.materialAssignments]
 * @property {Array<Connection>} [remove.connections]
 * @property {Object} [cut]
 * @property {Array<BlockInstance>} [remove.blockInstances]
 * @property {Array<MaterialAssignment>} [remove.materialAssignments]
 * @property {Array<Connection>} [remove.connections]
 * @property {Object} [add]
 * @property {Block} [add.block]
 * @property {Array<Object>} [add.materialAssignments]
 * @property {Array<Object>} [add.configurations]
 * @property {Array<Object>} [add.splitChainConfigurations]
 * @property {Array<Array<Connector>>} [add.connections] - Array of arrays with two connectors that should be connected
 */


/**
 * @typedef {Object} ConnectorSummary
 * @property {Connector} connector
 * @property {Block} block
 * @property {BlockInstance} blockInstance
 * @property {boolean} connected
 * @property {Connection} [connection]
 * @property {ConnectionRole} [role]
 * @property {Array<ConnectorType>} allowedMateTypes
 * @property {Array<Connector>} allowedMates
 */


/**
 * @name Configuration#blockInstances
 * @type {Array<BlockInstance>}
 */

/**
 * @typedef {Object} Placement
 * @property {Vector3} position
 * @property {Quaternion} quaternion
 */

/**
 * @typedef {Object} ConnectInstruction
 * @property {Object} from
 * @property {(Connectable|string)} from.connectable
 * @property {Connector} from.connector
 * @property {Object} to
 * @property {(Connectable|string)} to.connectable
 * @property {Connector} to.connector
 */


/**
 * @typedef {Object} InsertOption
 * @property {Block} block
 * @property {ConnectInstruction} newConnection1
 * @property {ConnectInstruction} newConnection2
 * @property {Connection} oldConnection
 * @property {Placement} placement
 */

/**
 * @typedef {Object} ExtendOption
 * @property {Block} block
 * @property {ConnectInstruction} [connection]
 * @property {Placement} placement
 */

/**
 * @typedef {Object} CutOption
 * @property {BlockInstance} blockInstance
 * @property {Array<Connection>} oldConnections
 * @property {Connection} [newConnection]
 * @property {Placement} placement
 */

/**
 * @typedef {Object} SplitOption
 * @property {BlockInstance} blockInstance
 * @property {Array<Connection>} oldConnections
 * @property {Array<Connectable>} siblingChains
 * @property {Placement} placement
 */

// bij laden material assignments aanmaken voor impliciete toewijzingen
// themes op default zetten

/**
 * Immutable data structure with methods to produce a new configuration
 */
class Configuration extends ComponentTree {

    /**
     * @param {Reporter} reporter
     * @param {Object} settings
     * @param {UUID} [settings.id]
     * @param {number} [settings.step = 0]
     * @param {Object<UUID,WrappedMaterial>} [settings.lastAssignedMaterials] last assigned material per material variant group
     * @param {Object<UUID,WrappedMaterial>} [settings.defaultMaterials] default material per material variant group
     * @param {string} [settings.creationDate]
     * @param {string} [settings.author]
     * @param {Package} settings.pkg
     * @param {Array<Connection>} [settings.connections]
     * @param {Object<string,Theme>} [settings.themes]
     * @param {Array<BlockInstance>} settings.blockInstances
     * @param {Array<MaterialAssignment>} [settings.materialAssignments]
     */

    constructor(reporter, settings) {

        // do basic type checking on the settings
        // and check that there are no duplicates

        settings.step = settings.step || 1;
        settings.blockInstances = settings.blockInstances || [];
        settings.connections = settings.connections || [];
        settings.materialAssignments = settings.materialAssignments || [];
        settings.themes = settings.themes || {};

        checkPropTypes(
            settings,
            {
                pkg: Package,
            },
            {

                blockInstances: val => {

                    if (!Array.isArray(val)) {
                        return 'Not an array';
                    }

                    for (let i = 0; i < val.length; i += 1) {
                        const bi = val[i];

                        if (!(bi instanceof BlockInstance)) {
                            return `Entry blockInstances[${i}] is not a BlockInstance, but a ${bi.constructor.name}`;
                        }
                    }

                    if (val.filter(unique).length !== val.length) {
                        return 'Duplicate blockInstances specified';
                    }

                    return true;
                },

                connections: val => {
                    if (!Array.isArray(val)) {
                        return 'Not an array';
                    }

                    for (let i = 0; i < val.length; i += 1) {
                        const conn = val[i];

                        if (!(conn instanceof Connection)) {
                            return `Entry connections[${i}] is not a Connection, but a ${conn.constructor.name}`;
                        }
                    }

                    if (val.filter(unique).length !== val.length) {
                        return 'Duplicate connections specified';
                    }

                    return true;
                },

                materialAssignments: val => {
                    if (!Array.isArray(val)) {
                        return 'Value is not an array'
                    }

                    for (let i = 0; i < val.length; i += 1) {
                        const ma = val[i];

                        if (!(ma instanceof MaterialAssignment)) {
                            return `Entry materialAssignments[${i}] is not a MaterialAssignment, but a ${ma.constructor.name}`;
                        }
                    }

                    if (val.filter(unique).length !== val.length) {
                        return 'Duplicate materialAssignments specified';
                    }

                    return true;
                }
            }
        );

        // if (settings.info === undefined) {
        //     settings.info = Configuration.generateInfoComponent(reporter, settings.pkg);
        // }

        const connectors = (settings.blockInstances || [])
            .reduce(
                (arr, bi) =>
                ([
                    ...arr,
                    ...((bi.block.connectors || []).map(cntr => ({ connectable: bi, connector: cntr })))
                ]),
                []
            );

        
        // console.log(connectors)
        // console.log(settings.pkg.themeGroups)

        for (let i = 0, l = settings.pkg.themeGroups.length || 0; i < l; i += 1) {

            const themeGroup = settings.pkg.themeGroups[i];

            // console.log(i, themeGroup)

            const theme = settings.themes[themeGroup.id] || settings.pkg.themes.find(t => t.group === themeGroup);

            // console.log('Constructor applying theme', theme.name )


            // this.applyTheme(blockInstances, connections, materialAssignments, theme);

            const themedConfigConnectorObjects = connectors.filter(cntrObj => cntrObj.connector.themeGroup === themeGroup);
            const themedConfigConnectors = themedConfigConnectorObjects.map(obj => obj.connector);

            const removedConnectableIds = [];

            const connectorObjectsToConnect = [...themedConfigConnectorObjects];

            // console.log(connectorObjectsToConnect, themedConfigConnectorObjects)


            // free the themed connectors by deleting their mates if they are not
            // connected to a connectable with the correct theme

            for (let connection of Object.values(settings.connections)) {

                // console.log('Check', connection.label);

                const fromThemedConnectorObject = themedConfigConnectorObjects.find(tcco => tcco.connector === connection.from.connector && tcco.connectable === connection.from.connectable);
                const toThemedConnectorObject = themedConfigConnectorObjects.find(tcco => tcco.connector === connection.to.connector && tcco.connectable === connection.to.connectable);

                if (fromThemedConnectorObject) {

                    if (themedConfigConnectors.includes(connection.to.connector)) {
                        throw new Error(`Both sides of ${connection.label} have a connector with ${themeGroup.label}: ${connection.to.connector.label}, ${connection.from.connector.label}`);
                    }

                    if (connection.to.connector.block.theme === theme) {

                        // this connector is already connected to a block with the selected theme, leave as is

                        // console.log(`${connection.label} already connected within theme`)

                        connectorObjectsToConnect.splice(
                            connectorObjectsToConnect.indexOf(fromThemedConnectorObject),
                            1
                        );
                    }
                    else {
                        // console.log('Constructor delete', connection.label, connection.to.connectable);

                        // delete settings.connections[connection.id];

                        settings.connections.splice(
                            settings.connections.indexOf(connection),
                            1
                        );

                        removedConnectableIds.push(connection.to.connectable.id);

                        // delete settings.blockInstances[connection.to.connectable.id];

                        settings.blockInstances.splice(
                            settings.blockInstances.indexOf(connection.to.connectable),
                            1
                        );

                    }
                }
                else if (toThemedConnectorObject) {

                    if (themedConfigConnectors.includes(connection.from.connector)) {
                        throw new Error(`Both sides of ${connection.label} have a connector with ${themeGroup.label}: ${connection.to.connector.label}, ${connection.from.connector.label}`);
                    }

                    if (connection.from.connector.block.theme === theme) {
                        // this connector is already connected to a block with the selected theme, ignore

                        // console.log(`${connection.label} already connected within theme`)

                        connectorObjectsToConnect.splice(
                            connectorObjectsToConnect.indexOf(toThemedConnectorObject),
                            1
                        );
                    }
                    else {

                        // console.log('Constructor delete', connection.label, connection.from.connectable)

                        // delete settings.connections[connection.id];

                        settings.connections.splice(
                            settings.connections.indexOf(connection),
                            1
                        );

                        removedConnectableIds.push(connection.from.connectable.id);

                        // delete settings.blockInstances[connection.from.connectable.id];

                        settings.blockInstances.splice(
                            settings.blockInstances.indexOf(connection.from.connectable),
                            1
                        );
                    }
                }
            }



            settings.materialAssignments = settings.materialAssignments.filter( ma => ! removedConnectableIds.includes( ma.blockInstance.id ))



            for (const themedConfigConnectorObject of connectorObjectsToConnect) {

                // console.log(themedConfigConnectorObject)

                if (removedConnectableIds.includes(themedConfigConnectorObject.connectable.id)) {
                    throw new Error(`Can't create a new connection for ${themedConfigConnectorObject.connectable.label} - it was removed`)
                }

                const mateConnector = settings.pkg.connectors.find(cntr => cntr.block.theme === theme && themedConfigConnectorObject.connector.mate.connectors.includes(cntr));

                const newBI = new BlockInstance(reporter, { block: mateConnector.block });

                // console.log('Added', newBI)

                settings.blockInstances.push(newBI);

                // const newConnection = this._createClonedComponentConnection(
                    const newConnection = new Connection(
                        reporter,
                        {
                            from: themedConfigConnectorObject,
                            to: {
                                connector: mateConnector,
                                connectable: newBI
                            }
                        },
                        // settings.blockInstances,
                        // newBI
                    );

                    settings.connections.push(newConnection);
                }
        }



        // call ComponentTree constructor without pkg in the settings
        // so it isn't autolinked by BuildableBlock instead, specify it
        // as an external tree in the instructions object


        super(
            reporter,
            omit(
                ['pkg'],
                settings
            ),
            {
                import: Configuration.importInstructions,
                externalTrees: [settings.pkg]
            }
        );


        this.options = {
            blocks: [],
            blockInstances: [],
            materials: []
        };

        this.instructions = {};

        this.clonedAssets = [];
        this.clonedComponents = [];

        // fucking typescript
        if (!Object.getOwnPropertyDescriptor(this, 'blockInstances')) {
            /** @type {Array<BlockInstance>} */
                this.blockInstances = [];
        }
        if (!Object.getOwnPropertyDescriptor(this, 'materialAssignments')) {
            /** @type {Array<MaterialAssignment>} */
                this.materialAssignments = [];
        }
        if (!Object.getOwnPropertyDescriptor(this, 'connections')) {
            /** @type {Array<Connection>} */
                this.connections = [];
        }


        //console.log( settings )

        this.pkg = settings.pkg;

        if (this.pkg.loader) {
            this.loader = this.pkg.loader;
        }


        // check that every connector is only connected once

        this.connectors = connectors;

        this.connectedConnectors = findConnectedConnectors(settings.connections);


        // calculate the placement (position and rotation) of every block instance

        if (Array.isArray(settings.blockInstances) && settings.blockInstances.length > 0) {
            this.placement = calculatePlacement(this);
        }
        else {
            this.placement = {};
            console.warn('Empty configuration detected, no block instances defined.')
        }


        // make implicit (default) materials explicit
        // materials can be assigned to block instances and positioned components with .configurable = true

        this.assignables = this.blockInstances.map(bi => bi.assignables).flat();


        this.assignedMaterials = {};

        // gather manipulation options for this configuration

        this.options = {
            extend: [],
            insert: [],
            replace: [],
            cut: [],
            split: [],
            blocks: [],
            blockInstances: [],
            materials: [],
            materialVariantGroups: [],
        };

        
        // connector dedup
        // remove connectors that are in the same location, because these are usually "inside" a configuration
        // that uses a grid to connect objects
        //
        // mechanism: 
        // for every connector
        // who's id isn't in the list already
        // find all colocated connectors
        // and add their ids to the list
        // then pass that list to the option finder
        // to ignore
        

        const colocatedConnectors = {};

        for ( let firstConnectorObject of connectors ) {

            //console.log(firstConnectorObject)

            const firstConnectablePlacement = this.placement[ firstConnectorObject.connectable.id ];
            const firstConnector = firstConnectorObject.connector;
            const firstConnectorPos = firstConnectablePlacement.position.clone().add(
                firstConnector.position.clone().applyQuaternion(firstConnectablePlacement.quaternion)
            );

            //console.log('find co-located connectors of connector', firstConnector.id); //, firstConnectablePos, firstConnectorPos);
        
            const coloConns = connectors.filter( 

                secondConnectorObject => {

                    // potentially co-located connector
                    const secondConnector = secondConnectorObject.connector;

                    if ( secondConnector.id === firstConnector.id ) {
                        // same connector, next
                        return false;
                    }

                    //console.log('check against', secondConnector.id); //, secondConnector.position, firstConnector.position);

                    try {

                        const secondConnectablePlacement = this.placement[ secondConnectorObject.connectable.id ];
                        const secondConnectorPos = secondConnectablePlacement.position.clone().add(
                            secondConnector.position.clone().applyQuaternion(secondConnectablePlacement.quaternion)
                        );

                        const dist = firstConnectorPos.distanceTo(secondConnectorPos);

                        //console.log ( 'connectable positions', firstConnectablePlacement, secondConnectablePlacement );
                        //console.log ( 'connector rel positions', firstConnector.position, secondConnector.position );
                        //console.log ( 'connector abs positions', firstConnectorPos, secondConnectorPos );

                        //console.log( 'dist', dist, 'colocated', dist < 0.01);

                        return dist < 0.01;

                    } catch ( err ) {
                        console.error(err);
                    }
                }
            );

            for ( let coloConn of coloConns ) {
                // store a reference to the connector and connectable
                colocatedConnectors[ coloConn.connector.id ] = colocatedConnectors[ coloConn.connector.id ] || {};
                colocatedConnectors[ coloConn.connector.id ][ coloConn.connectable.id ] = true;
            }
        }

        //console.log( 'clcids', colocatedConnectors )

        this.options.extend = findExtendOptions(this, colocatedConnectors);
        this.options.insert = findInsertOptions(this);




        this.options.extend.forEach(option => this._addBlockAddOption(option, 'extend'));
        this.options.insert.forEach(option => this._addBlockAddOption(option, 'insert'));

        const { cutOptions, splitOptions } = findBlockInstanceOptions(this);

        this.options.cut = cutOptions;
        this.options.split = splitOptions;

        this.options.cut.forEach(option => this._addBlockInstanceRemoveOption(option, 'cut'));
        this.options.split.forEach(option => this._addBlockInstanceRemoveOption(option, 'split'));

        for (let bi of this.blockInstances) {

            for (let mvg of bi.block.distinctMVGs) {

                if (!this.options.materialVariantGroups.includes(mvg)) {
                    // console.log('new', mvg.slug)
                    this.options.materialVariantGroups.push(mvg);
                    // this.options.blockInstancesPerMVG[ mvg.id ] = [];
                    this.options[mvg.id] = [];
                }

                this.options[mvg.id].push(bi);
            }
        }

        // this.options.materialVariantGroups = this.options.materialVariantGroups.filter(unique);

        const mvgIdCacheKey = this.options.materialVariantGroups.map(mvg => mvg.id).sort().join('-');

        if (!Configuration.materialCache[mvgIdCacheKey]) {
            const configuration = this;
            const materials = this.options.materialVariantGroups.map(mvg => mvg.allMaterials).flat().filter(unique);

            Configuration.materialCache[mvgIdCacheKey] = materials;
        }

        this.options.materials = Configuration.materialCache[mvgIdCacheKey];

        this.options.material = material => {

            console.assert(material instanceof WrappedMaterial, `${material.label || typeof material} is not an instance of WrappedMaterial`);

            const assignables = this.assignables.filter(assignable => assignable.materialVariantGroups.find(mvg => material.materialSets.includes(mvg)));

            return assignables.map(assignable => {

                const assigningComponent = getAssigningComponent(assignable);

                const placement = {
                    position: new Vector3().copy(this.placement[assignable.blockInstance.id].position),
                    quaternion: new Quaternion().copy(this.placement[assignable.blockInstance.id].quaternion),
                };

                if (assignable.positionedMeshGroup) {
                    placement.position.add(assignable.positionedMeshGroup.position);
                    placement.quaternion.multiply(assignable.positionedMeshGroup.quaternion);
                }

                if (assignable.positionedMesh) {
                    placement.position.add(assignable.positionedMesh.position);
                    placement.quaternion.multiply(assignable.positionedMesh.quaternion);
                }

                return {
                    assignable,
                    material,
                    placement
                };

            });
        };

        //create map to store the boudingboxes for the blockInstances
        this.boundingBoxes = new Map();

        //create array to store alle the vertices of all the blockInstances
        const boundingBoxVertices = []

        //place boundingBox and add boundingBox to the map and its vertices to the vertices array
        for (let blockInstance of settings.blockInstances || []) {

            const bboxGeometry = blockInstance.boundingBox.clone();

            this.clonedAssets.push(bboxGeometry);

            const matrix = new Matrix4()
            const pos = this.placement[blockInstance.id].position
            const qat = this.placement[blockInstance.id].quaternion
            const scale = new Vector3(1, 1, 1)
            matrix.compose(pos, qat, scale)
            bboxGeometry.applyMatrix4(matrix)

            blockInstance.boundingBox = bboxGeometry

            this.boundingBoxes.set(blockInstance.id, bboxGeometry)

            blockInstance.boundingBox.computeBoundingBox()
            blockInstance.boundingBox.computeBoundingSphere()

            //vertex positions
            const vecticePositions = bboxGeometry.attributes.position;
            const vector = new Vector3();

            for (let i = 0, l = vecticePositions.count; i < l; i++) {

                vector.fromBufferAttribute(vecticePositions, i);
                const vecClone = vector.clone();
                this.clonedAssets.push(vecClone);
                boundingBoxVertices.push(vecClone);

            }

        }

        //compute boudningBox for the configuration based on the min / max of all the vertice points from the boundingBoxVertices array
        var vectorMin = new Vector3();
        var vectorMax = new Vector3();

        //get the min and max positions
        for (let vertice of boundingBoxVertices) {
            vectorMin.min(vertice)
            vectorMax.max(vertice)
        }

        //create axes alligned boundingbox
        const boundingBox = new Box3(vectorMin, vectorMax)

        //create 8 vertice corner points based on box3
        const vertices = new Float32Array([
            boundingBox.min.x, boundingBox.min.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.min.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.min.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.min.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.max.y, boundingBox.min.z,
            boundingBox.min.x, boundingBox.max.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.max.y, boundingBox.max.z,
            boundingBox.max.x, boundingBox.max.y, boundingBox.min.z
        ]);

        //create geometry to store the vertices
        const boundingBoxGeometry = new BufferGeometry()
        boundingBoxGeometry.name = "boundingBox";
        boundingBoxGeometry.visible = false;
        boundingBoxGeometry.setAttribute('position', new BufferAttribute(vertices, 3));
        boundingBoxGeometry.computeBoundingBox()
        boundingBoxGeometry.computeBoundingSphere()

        //add the buffergeometry as boudingbox property to the configuration
        this.boundingBox = boundingBoxGeometry

        //console.log( this.boundingBox )
        //console.log( this.boundingBoxes )

        //compute dimesion lines
        this.dimensions = getDimensions(this)
        // console.log(this.dimensions)

        const config = this;

    }


    static _exportName = {
        singular: 'configuration',
        plural: 'configurations'
    };

    static importInstructions = [
        {
            cls: BlockInstance
        },
        {
            cls: Connection
        },
        {
            cls: MaterialAssignment
        }
    ];

    static materialCache = {};

    // static generateInfoComponent(reporter, pkg) {
        //     return new ConfigurationInfo(
            //         reporter,
            //         {
                //             name: pkg.info.name + ' Configuration',
                //             packageId: pkg.id,
                //             packageURL: pkg.loader.loadingBases[0].url,
                //             step: 0
                //         }
            //     );
        // }




    /**
        * All (wrapped) meshes that are present (at least once) in this configuration
        * @type {Array<WrappedMesh>} 
        */

        allMeshes;


    /**
        * All material variant groups that are applicable on (at least one mesh in) this configuration
        * @type {Array<MaterialSet>} 
        */

        applicableMatVarGroups;


    /**
        * All materials that are applicable on (at least one mesh in) this configuration
        * @type {Array<Material>} 
        */

        applicableMaterials;


    /** 
        * Connectors in this configuration
        * Array of objects the have the connectable and the connector, as that is the unique combination,
        * a connector on its own is not unique.
        * @type {Array<Object<Connectable,Connector>>}
        */

        connectors;



    /** @type {Array<ExtendOption>} */

        extendOptions;


    /** @type {Array<InsertOption>} */

        insertOptions;


    /**
        * List of components that can be used "on" this configuration
        * @type {Object}
        */

        options;


    /**
        * List of how-to-use instructions per component
        * @type {Object}
        */

        instructions;


    assignedMaterials;


    _addBlockAddOption(addOption, type) {

        const block = addOption.block;

        if (this.options.blocks.indexOf(block) === -1) {
            this.options.blocks.push(block);
        }

        if (!this.options[block.id]) {
            this.options[block.id] = { extend: [], insert: [] };
        }

        this.options[block.id][type].push(addOption);
    }

    _addBlockInstanceRemoveOption(removeOption, type) {

        const blockInstance = removeOption.blockInstance;

        if (this.options.blockInstances.indexOf(blockInstance) === -1) {
            this.options.blockInstances.push(blockInstance);
        }

        if (!this.options[blockInstance.id]) {
            this.options[blockInstance.id] = { cut: [], split: [], replace: [], materialize: [] };
        }

        this.options[blockInstance.id][type].push(removeOption);
    }


    preparedComponentClones;


    prepareComponentClones() {
        // console.log('prepare component clones');
        this.preparedComponentClones = null;
        this.cloneComponents();
        // this.preparedComponentClones = this.cloneComponents();
        // console.log('prep done')
    }


    cloneComponents(equalBlockInstanceIds = true) {

        if (this.preparedComponentClones) {
            console.log('use prepared component clones');
            const preparedComponentClones = this.preparedComponentClones;
            this.preparedComponentClones = null;
            return preparedComponentClones;
        }

        let blockInstanceClones = {};
        let materialAssignmentClones = {};
        let connectionClones = {};

        for (let bi of this.blockInstances) {

            // when creating a new config, the standard is to re-use the
            // block instance ids, so transforms can find the meshes again 
            // and relink 

            const clonedBi = bi.clone(equalBlockInstanceIds === true ? { id: bi.id } : {});
            // console.log(bi, clonedBi)
            blockInstanceClones[bi.id] = clonedBi;
        }

        for (let ma of this.materialAssignments) {
            const newMa = new MaterialAssignment(
                this._reporter,
                {
                    ...ma.settings,
                    blockInstance: blockInstanceClones[ma.blockInstance.id]
                });
            materialAssignmentClones[ma.id] = newMa;
        }

        for (let cn of this.connections) {
            const newCn = new Connection(
                this._reporter,
                {
                    from: {
                        connectable: blockInstanceClones[cn.from.connectable.id],
                        connector: cn.from.connector
                    },
                    to: {
                        connectable: blockInstanceClones[cn.to.connectable.id],
                        connector: cn.to.connector
                    },
                    quaternion: cn.quaternion
                }
            );
            connectionClones[cn.id] = newCn;
        }



        return {
            blockInstanceClones,
            materialAssignmentClones,
            connectionClones
        }
    }

    // addBlockInstance()

    /**
        * @param {Object} newConnectionData 
        * @param {Object<UUID,BlockInstance>} blockInstanceClones 
        * @param {BlockInstance} [newBlockInstance]
        */

        _createClonedComponentConnection(newConnectionData, blockInstanceClones, newBlockInstance) {
            return new Connection(
                this._reporter,
                {
                    from: {
                        connectable: newConnectionData.from.connectable === 'new' ? newBlockInstance : blockInstanceClones[newConnectionData.from.connectable.id],
                        connector: newConnectionData.from.connector
                    },
                    to: {
                        connectable: newConnectionData.to.connectable === 'new' ? newBlockInstance : blockInstanceClones[newConnectionData.to.connectable.id],
                        connector: newConnectionData.to.connector
                    },
                });
        }

    _createNewConfiguration(blockInstances, materialAssignments, connections, settings = {}) {

        const newSettings = {};

        Object.assign(
            newSettings,
            this.settings
        );

        newSettings.id = uuid();

        Object.assign(
            newSettings,
            settings
        );

        newSettings.blockInstances = blockInstances;
        newSettings.materialAssignments = materialAssignments;
        newSettings.connections = connections;
        newSettings.pkg = this.pkg;

        newSettings.step = (Number(newSettings.step) || 0) + 1;

        return new Configuration(
            this._reporter,
            newSettings
        );
    }

    _createLastAssignedMAsForBlockInstance(newBI) {
        const newMAs = {};
        if (this.lastAssignedMaterials) {
            const relevantLAMaterials = pick(newBI.applicableMaterialVariantGroups.map(mvg => mvg.id), this.lastAssignedMaterials);
            for (let relevantLAMaterial of Object.values(relevantLAMaterials)) {
                const repeatMA = new MaterialAssignment(
                    this._reporter,
                    {
                        blockInstance: newBI,
                        material: relevantLAMaterial
                    }
                );
                newMAs[repeatMA.id] = repeatMA;
            }
        }
        return newMAs;
    }


    add() {
        // for empty config?
    }


    /**
        * @param {ExtendOption} option 
        */

        extend(option, instructions = {}) {
            console.log('extend option', option)
            let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

            // add block
            const newBI = new BlockInstance(this._reporter, { block: option.block });
            blockInstanceClones[newBI.id] = newBI;

            // add connection, if necessary
            if (option.connection) {
                const newConnection = this._createClonedComponentConnection(option.connection, blockInstanceClones, newBI);
                connectionClones[newConnection.id] = newConnection;
            }

            if (instructions.repeatLastAssignment === true) {
                Object.assign(
                    materialAssignmentClones,
                    this._createLastAssignedMAsForBlockInstance(newBI)
                );
            }

            // return new config
            const nc = this._createNewConfiguration(
                Object.values(blockInstanceClones),
                Object.values(materialAssignmentClones),
                Object.values(connectionClones),
            );

            return nc
        }

    insert(option, instructions = {}) {
        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        // add block
        const newBI = new BlockInstance(this._reporter, { block: option.block });
        blockInstanceClones[newBI.id] = newBI;

        // add 2x connection
        const newConnection1 = this._createClonedComponentConnection(option.newConnection1, blockInstanceClones, newBI);
        connectionClones[newConnection1.id] = newConnection1;

        const newConnection2 = this._createClonedComponentConnection(option.newConnection2, blockInstanceClones, newBI);
        connectionClones[newConnection2.id] = newConnection2;

        // remove old connection
        delete connectionClones[option.oldConnection.id];

        if (instructions.repeatLastAssignment === true) {
            Object.assign(
                materialAssignmentClones,
                this._createLastAssignedMAsForBlockInstance(newBI)
            );
        }

        // return new config
        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            Object.values(materialAssignmentClones),
            Object.values(connectionClones),
        );
    }

    replace() {
        // remove block
        // remove old connections
        // add block
        // add new connections
    }



    copy(equalBlockInstanceIds = true) {
        let {
            blockInstanceClones,
            materialAssignmentClones,
            connectionClones
        } = this.cloneComponents(equalBlockInstanceIds);

        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            Object.values(materialAssignmentClones),
            Object.values(connectionClones)
        );
    }

    _cut(option) {
        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        const cutConfigurationComponents = {
            blockInstances: [],
            materialAssignments: [],
            connections: []
        };

        // cut block
        cutConfigurationComponents.blockInstances.push(blockInstanceClones[option.blockInstance.id]);
        delete blockInstanceClones[option.blockInstance.id];

        // cut block children
        for (let child of option.blockInstance.children) {
            cutConfigurationComponents.blockInstances.push(blockInstanceClones[child.id]);
            delete blockInstanceClones[child.id];
        }

        // cut block and childrens' material assignments
        for (let ma of [...option.blockInstance.materialAssignments, ...(option.blockInstance.childMaterialAssignments || [])]) {
            cutConfigurationComponents.materialAssignments.push(materialAssignmentClones[ma.id]);
            delete materialAssignmentClones[ma.id];
        }

        // cut child connections
        for (let cn of option.blockInstance.childConnections) {
            cutConfigurationComponents.connections.push(connectionClones[cn.id]);
            delete connectionClones[cn.id];
        }

        // delete old connections
        for (let oldConnection of option.oldConnections) {
            delete connectionClones[oldConnection.id];
        }

        return { cutConfigurationComponents, blockInstanceClones, materialAssignmentClones, connectionClones };
    }

    cut(option) {
        const { cutConfigurationComponents, blockInstanceClones, materialAssignmentClones, connectionClones } = this._cut(option);

        // create new connection, if necessary
        if (option.newConnection) {
            const newConnection = this._createClonedComponentConnection(option.newConnection, blockInstanceClones);
            connectionClones[newConnection.id] = newConnection;
        }

        // create new configuration
        return {
            main: this._createNewConfiguration(
                Object.values(blockInstanceClones),
                Object.values(materialAssignmentClones),
                Object.values(connectionClones),
            ),
            cut: this._createNewConfiguration(
                cutConfigurationComponents.blockInstances,
                cutConfigurationComponents.materialAssignments,
                cutConfigurationComponents.connections,
            )
        };
    }

    split(option) {
        const { cutConfigurationComponents, blockInstanceClones, materialAssignmentClones, connectionClones } = this._cut(option);

        const chainConfigurations = [];

        // console.log(option.siblingChains)

        for (let chain of Object.values(option.siblingChains)) {

            const chainComponents = chain.reduce(
                (obj, connectable) => {

                    obj.blockInstances.push(connectable, ...(connectable.children || []));

                    if (connectable.materialAssignments && connectable.materialAssignments.length > 0) {
                        obj.materialAssignments.push(...connectable.materialAssignments);
                    }

                    if (connectable.childMaterialAssignments && connectable.childMaterialAssignments.length > 0) {
                        obj.materialAssignments.push(...connectable.childMaterialAssignments);
                    }

                    if (connectable.childConnections && connectable.childConnections.length > 0) {
                        obj.connections.push(...connectable.childConnections);
                    }

                    const inChainConnections = connectable.equalConnections.filter(conn => !option.oldConnections.includes(conn));

                    for (let inChainConnection of inChainConnections) {
                        if (obj.connections.indexOf(inChainConnection) === -1) {
                            obj.connections.push(inChainConnection);
                        }
                    }


                    if (connectable.childConnections && connectable.childConnections.length > 0) {
                        obj.connections.push(...connectable.childConnections);
                    }

                    return obj;
                },
                {
                    blockInstances: [],
                    materialAssignments: [],
                    connections: [],
                }
            );

            chainComponents.blockInstances = chainComponents.blockInstances.map(bi => blockInstanceClones[bi.id]);
            chainComponents.materialAssignments = chainComponents.materialAssignments.map(ma => materialAssignmentClones[ma.id]);
            chainComponents.connections = chainComponents.connections.map(cn => connectionClones[cn.id]);

            chainConfigurations.push(
                this._createNewConfiguration(
                    chainComponents.blockInstances,
                    chainComponents.materialAssignments,
                    chainComponents.connections,
                    this.step + 1
                )
            );
        }

        return {
            cut: this._createNewConfiguration(
                cutConfigurationComponents.blockInstances,
                cutConfigurationComponents.materialAssignments,
                cutConfigurationComponents.connections,
            ),
            split: chainConfigurations
        }
    }


    /**
        * Copies another Configuration and links it to this one
        * @param {BlockInstance} myBlockInstance 
        * @param {Connector} myConnector 
        * @param {BlockInstance} otherBlockInstance
        * @param {Connector} otherConnector 
        */

        merge(myBlockInstance, myConnector, otherBlockInstance, otherConnector) {  // mergeOption?

                const otherConfiguration = otherBlockInstance.tree;

            if (otherConfiguration.pkg !== this.pkg) {
                throw new Error(`Can not merge configuration for ${this.pkg.label} with configurator for ${otherConfiguration.pkg.label} `);
            }

            const myComponentsCloned = this.cloneComponents();
            const otherComponentsCloned = otherConfiguration.cloneComponents();

            // console.log(myBlockInstance, myConnector, otherBlockInstance, otherConnector)

            const newConnection = this._createClonedComponentConnection(
                {
                    from: { connectable: myBlockInstance, connector: myConnector },
                    to: { connectable: otherBlockInstance, connector: otherConnector }
                },
                {
                    ...myComponentsCloned.blockInstanceClones,
                    ...otherComponentsCloned.blockInstanceClones
                }
            );

            const allComponents = {
                blockInstances: [
                    ...Object.values(myComponentsCloned.blockInstanceClones || {}),
                    ...Object.values(otherComponentsCloned.blockInstanceClones || {}),
                ],
                materialAssignments: [
                    ...Object.values(myComponentsCloned.materialAssignmentClones || {}),
                    ...Object.values(otherComponentsCloned.materialAssignmentClones || {}),
                ],
                connections: [
                    ...Object.values(myComponentsCloned.connectionClones || {}),
                    ...Object.values(otherComponentsCloned.connectionClones || {}),
                    newConnection
                ],
            };

            return this._createNewConfiguration(
                allComponents.blockInstances,
                allComponents.materialAssignments,
                allComponents.connections,
            );
        }


    findMergeOptions(otherConfiguration) {
        // compare unconnected connectors on both configurations
    }



    assignMaterial(option) {

        // console.log(option)

        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        const newMaSettings = {};

        newMaSettings.blockInstance = blockInstanceClones[option.assignable.blockInstance.id]

        if (option.assignable.positionedMeshGroup) {
            newMaSettings.positionedMeshGroup = option.assignable.positionedMeshGroup; //blockInstanceClones[option.assignable.positionedMeshGroup.id];
        }

        if (option.assignable.positionedMesh) {
            newMaSettings.positionedMesh = option.assignable.positionedMesh; //blockInstanceClones[option.assignable.positionedMesh.id];
        }

        newMaSettings.material = option.material;

        const newMA = new MaterialAssignment(
            this._reporter,
            newMaSettings
        );

        // delete old assignment for the assigning component

        const assigningComponent = option.assignable.assigningComponent;

        for (let [id, ma] of Object.entries(materialAssignmentClones)) {
            if (ma.assigningComponent.id === assigningComponent.id) {
                if (newMA.affectedComponents.length === ma.affectedComponents.length && !newMA.affectedComponents.find(ac => ma.affectedComponents.includes(ac) === undefined)) {
                    delete materialAssignmentClones[id];
                }
            }
        }

        materialAssignmentClones[newMA.id] = newMA;

        const lastAssignedMaterials = this.lastAssignedMaterials || {};

        // not every set is necessarily a mvg, but ok
        for (let mvg of newMaSettings.material.materialSets) {
            lastAssignedMaterials[mvg.id] = newMaSettings.material;
        }

        // console.log(materialAssignmentClones)

        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            Object.values(materialAssignmentClones),
            Object.values(connectionClones),
            {
                lastAssignedMaterials
            }
        );
    }


    setDefaultMaterial(material) {

        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        const defaultMaterials = this.defaultMaterials || {};

        // not every set is necessarily a mvg, but ok
        for (let mvg of material.materialSets) {
            defaultMaterials[mvg.id] = material;
        }

        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            Object.values(materialAssignmentClones),
            Object.values(connectionClones),
            {
                defaultMaterials
            }
        );
    }

    clearMaterialAssignments() {
        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            [],
            Object.values(connectionClones)
        );
    }


    // setTheme(blockInstances, connections, materialAssignments, theme) {

        //     const themeGroup = theme.group;
        //     const themedConfigConnectorObjects = this.connectors.filter(cntrObj => cntrObj.connector.themeGroup === themeGroup);
        //     const themedConfigConnectors = themedConfigConnectorObjects.map(obj => obj.connector);

        //     const removedConnectableIds = [];

        //     const connectorObjectsToConnect = [...themedConfigConnectorObjects];


        //     // free the themed connectors by deleting their mates if they are not
        //     // connected to a connectable with the correct theme

        //     for (let connection of Object.values(this.connections)) {

            //         const fromThemedConnectorObject = themedConfigConnectorObjects.find(tcco => tcco.connector === connection.from.connector && tcco.connectable === connection.from.connectable);
            //         const toThemedConnectorObject = themedConfigConnectorObjects.find(tcco => tcco.connector === connection.to.connector && tcco.connectable === connection.to.connectable);

            //         if (fromThemedConnectorObject) {

                //             if (themedConfigConnectors.includes(connection.to.connector)) {
                    //                 throw new Error(`Both sides of ${connection.label} have a connector with ${themeGroup.label}: ${connection.to.connector.label}, ${connection.from.connector.label}`);
                    //             }

                //             if (connection.to.connector.block.theme === theme) {

                    //                 // this connector is already connected to a block with the selected theme, leave as is

                    //                 console.log(`${connection.label} already connected within theme`)

                    //                 connectorObjectsToConnect.splice(
                        //                     connectorObjectsToConnect.indexOf(fromThemedConnectorObject),
                        //                     1
                        //                 );
                    //             }
                //             else {
                    //                 delete connections[connection.id];

                    //                 removedConnectableIds.push(blockInstances[connection.to.connectable.id].id);

                    //                 delete blockInstances[connection.to.connectable.id];
                    //             }
                //         }
            //         else if (toThemedConnectorObject) {

                //             if (themedConfigConnectors.includes(connection.from.connector)) {
                    //                 throw new Error(`Both sides of ${connection.label} have a connector with ${themeGroup.label}: ${connection.to.connector.label}, ${connection.from.connector.label}`);
                    //             }

                //             if (connection.to.connector.block.theme === theme) {
                    //                 // this connector is already connected to a block with the selected theme, ignore

                    //                 console.log(`${connection.label} already connected within theme`)

                    //                 connectorObjectsToConnect.splice(
                        //                     connectorObjectsToConnect.indexOf(toThemedConnectorObject),
                        //                     1
                        //                 );
                    //             }
                //             else {

                    //                 delete connections[connection.id];

                    //                 removedConnectableIds.push(blockInstances[connection.from.connectable.id].id);

                    //                 delete blockInstances[connection.from.connectable.id];
                    //             }
                //         }
            //     }

        //     for (let ma of Object.values(materialAssignments)) {

            //         if (removedConnectableIds.includes(ma.blockInstance)) {
                //             delete materialAssignments[ma.id];
                //         }
            //     }

        //     for (const themedConfigConnectorObject of connectorObjectsToConnect) {

            //         if (removedConnectableIds.includes(themedConfigConnectorObject.connectable.id)) {
                //             throw new Error(`Can't create a new connection for ${themedConfigConnectorObject.connectable.label} - it was removed`)
                //         }

            //         const mateConnector = this.pkg.connectors.find(cntr => cntr.block.theme === theme && themedConfigConnectorObject.connector.mate.connectors.includes(cntr));

            //         const newBI = new BlockInstance(this._reporter, { block: mateConnector.block });

            //         blockInstances[newBI.id] = newBI;

            //         const newConnection = this._createClonedComponentConnection(
                //             {
                    //                 from: themedConfigConnectorObject,
                    //                 to: {
                        //                     connector: mateConnector,
                        //                     connectable: 'new'
                        //                 }
                    //             },
                //             blockInstances,
                //             newBI
                //         );

            //         connections[newConnection.id] = newConnection;
            //     }

        //     // return { blockInstances, connections, materialAssignments }

        // }


    applyTheme(theme) {

        if (!(theme instanceof Theme)) {
            throw new Error(`${theme} is not a Theme`);
        }

        //console.log('apply theme', theme.name)

        let { blockInstanceClones, materialAssignmentClones, connectionClones } = this.cloneComponents();

        // create a new theme settings object without
        // changing the current settings

        const configThemeSetting = this._settings.themes || {};

        const newConfigThemeSetting = {};

        Object.assign(newConfigThemeSetting, configThemeSetting);

        newConfigThemeSetting[theme.group.id] = theme;


        return this._createNewConfiguration(
            Object.values(blockInstanceClones),
            Object.values(materialAssignmentClones),
            Object.values(connectionClones),
            {
                themes: newConfigThemeSetting
            }
        );
    }


    destroy() {

        // Always dispose Three.js cloned assets (geometry buffers, etc.)
        // to prevent GPU memory leaks. This must happen regardless of
        // whether this config is a preset.
        for (let asset of this.clonedAssets) {
            if (asset.dispose) {
                asset.dispose();
            }
        }

        for (let elem of this.blockInstances) {
            elem.destroy();
        }
        for (let elem of this.materialAssignments) {
            elem.destroy();
        }
        for (let elem of this.connections) {
            elem.destroy();
        }

        // Guard: if this config is a package preset, skip full teardown
        // (nuking properties) but resources have already been disposed above.
        if (this.pkg) {
            for (let config of this.pkg.configurations) {
                if (this.id === config.id) {
                    return;
                }
            }
        }

        if (super.destroy) {
            super.destroy();
        }
    }

    /**
        * @param {ComponentPart} part 
        * @param {LoadingQuality} quality 
        * @param {Object<string,Component>} dependencies 
        * @returns {Promise<Component>}
        */

        async _build(part, quality, dependencies) {

            const configuration = this;

            switch (part) {

                case 'UI':

                    if (this._settings.thumbnail) {
                        this._setContent('UI', quality, this._settings.thumbnail.content.main[quality]);
                    }
                    else {
                        this._setContent('UI', quality, Project.defaultImages.missing.cloneNode(true));
                    }
                    break;

                case 'main':

                    // This Group is the THREE.Object3D that will hold the "sceneable" configuration

                    const group = new Group();

                    group.castShadow = true;
                    group.receiveShadow = true;

                    const bboxLayer = Project.layerMap.boundingBoxes

                    /** @type {string} */

                        group.name = `Build of ${this.label}`;


                    const defaultMaterials = Object.values(this.defaultMaterials || {}).filter(unique);

                    const allVertices = []

                    for (let blockInstance of this.blockInstances) {

                        console.assert(blockInstance.bestMainContent instanceof Object3D);
                        // console.log(blockInstance.label, blockInstance.block.label)
                        // console.log(this.placement)

                        const obj3D = blockInstance.bestMainContent;

                        obj3D.position.copy(this.placement[blockInstance.id].position);
                        obj3D.quaternion.copy(this.placement[blockInstance.id].quaternion);

                        // console.log(this.materialAssignments.map(ma => ma.blockInstance))

                        const applicableMaterialAssignments = (this.materialAssignments || []).filter(ma => ma.blockInstance === blockInstance);

                        // sort from most to least specific
                        const sortedAssignments = applicableMaterialAssignments
                            .sort((a, b) => a.affectedComponents.length - b.affectedComponents.length);

                        const assignfromDefault = [];

                        for (let defaultMaterial of defaultMaterials) {

                            assignfromDefault.push({
                                material: defaultMaterial,
                                affectedComponents: blockInstance.materializableDependencies
                                .filter(mc =>
                                    mc instanceof PositionedComponent && mc.component.materialVariantGroup.has(defaultMaterial)
                                )
                            })
                        }


                        // assign materials to meshes
                        // keep a list of processed meshes, to prevent double assigning

                        const materializedComponentIds = [];

                        for (let ma of [...sortedAssignments, ...assignfromDefault]) {
                            // console.log( 'apply', ma.label)
                            for (let ac of ma.affectedComponents) {
                                // console.log( 'on?', ac.label)
                                if (!materializedComponentIds.includes(ac.id)) {
                                    // console.log( 'yes')
                                    materializedComponentIds.push(ac.id);
                                    const meshToMaterialize = obj3D.getObjectByName(ac.id);
                                    //console.log( meshToMaterialize )

                                    //hier nog de tiling multiplier toepassen van de mesh
                                    //alle meshes ophalen en de bijbehorende tiling multiplpiers
                                    const posMesh = this._tree.findComponentById( meshToMaterialize.userData.PB.origin ) 
                                    //console.log( posMesh )

                                    const wrappedMesh = posMesh.dependencies.main.component 
                                    //console.log( wrappedMesh )

                                    if ( ! posMesh) {
                                        console.warn('Tree referenced in upcoming error', this._tree);
                                        throw new Error(`Could not find positioned mesh with id ${meshToMaterialize.userData.PB.origin} in tree`)
                                    }
                                    var tilingMultiplier = null;
                                    for (let child of posMesh._allDependencies) {
                                        if (child instanceof WrappedMesh) {
                                            tilingMultiplier = child._tilingMultiplier
                                        }
                                    }


                                    //get the current product maps form the mesh material
                                    const aoMap = meshToMaterialize.material.aoMap;
                                    const aoMApIntensity = meshToMaterialize.material.aoMapIntensity;
                                    var normalMapRepeat = null;

                                    //copy normalMapRepeat because of bug in threejs mapClone function
                                    // console.log( ma.material.content.main[quality].normalMap )
                                    if (ma.material.content.main[quality].normalMap instanceof Texture) {
                                        normalMapRepeat = ma.material.content.main[quality].normalMap.repeat
                                        //console.log( normalMapRepeat )
                                    }

                                    //check if current mesh needs a node material 
                                    if( wrappedMesh._normalMap ){

                                        console.log( 'node material')
                                        meshToMaterialize.material = await wrappedMesh.buildMeshMaterial( ma.material.content.main[quality].clone() )
                                        meshToMaterialize.material.needsUpdate = true;

                                    }
                                    else{

                                        //apply new material
                                        meshToMaterialize.material = ma.material.content.main[quality].clone();

                                    }

                                    //apply product maps from previous mesh material
                                    meshToMaterialize.material.aoMap = aoMap
                                    meshToMaterialize.material.aoMapIntensity = aoMApIntensity
                                    if (meshToMaterialize.material.aoMap instanceof Texture) {
                                        meshToMaterialize.material.aoMap.needsUpdate = true;
                                    }

                                    //apply tiling multiplier to maps
                                    for (let mapType of ['bumpMap', 'emissiveMap', 'map', 'specularMap', 'roughnessMap', 'metalnessMap', 'normalMap']) {
                                        if (meshToMaterialize.material[mapType] instanceof Texture) {

                                            const mapClone = meshToMaterialize.material[ mapType ].clone();
                                            // const mapClone = meshToMaterialize.material[mapType];

                                            this.clonedAssets.push(mapClone);

                                            // console.log( mapClone )
                                            mapClone.repeat.x = mapClone.repeat.x * tilingMultiplier
                                            mapClone.repeat.y = mapClone.repeat.y * tilingMultiplier
                                            mapClone.needsUpdate = true;

                                            meshToMaterialize.material[mapType] = mapClone;

                                            // K: Joost removed this, unneccesary?
                                                // meshToMaterialize.material.needsUpdate = true;
                                        }
                                        if (meshToMaterialize.material['normalMap'] instanceof Texture) {
                                            meshToMaterialize.material['normalMap'].repeat = normalMapRepeat;
                                        }
                                    }


                                    meshToMaterialize.material.needsUpdate = true;

                                    //console.log( meshToMaterialize )

                                }
                            }
                        }



                        // populate assigned material list

                        this.assignedMaterials[blockInstance.id] = blockInstance.assignables.map(assignable => ({
                            assignable,
                            materials: [],
                            mvg: assignable.materialVariantGroups.reduce((obj, mvg) => Object.assign(obj, { [mvg.id]: null }), {})
                        })
                        );

                        obj3D.traverse(subObj => {

                            if (subObj.material) {
                                const assignable = subObj.userData.PB.assignable;
                                const materialId = subObj.material.userData.PB.origin;

                                if (!assignable) {
                                    console.error(subObj);
                                    throw new Error(`Missing assignable on Object3D ${subObj.id}`);
                                }

                                const material = this._tree.findComponentById(subObj.material.userData.PB.origin);

                                const assignedMaterialsEntry = this.assignedMaterials[blockInstance.id].find(assMatEntry => assMatEntry.assignable === assignable);

                                if (!assignedMaterialsEntry) {
                                    console.error(assignable);
                                    throw new Error('Could not find assigned materials list entry');
                                }

                                if (assignedMaterialsEntry.materials.indexOf(material) === -1) {

                                    assignedMaterialsEntry.materials.push(material);

                                    for (let set of material.materialSets) {

                                        // the reason for this check, is that material.materialSets
                                        // returns all sets the material is in, not just the ones that
                                        // are material variant groups

                                        // if ( assignedMaterialsEntry.mvg[ set.id ] !== undefined ) {
                                            assignedMaterialsEntry.mvg[set.id] = material;
                                            // }
                                    }
                                }
                            }
                        });






                        // console.log(this.assignedMaterials[ blockInstance.id ]);

                        // traverse 3d structure, keep track of actual materials per assignable per mvg
                        // find obj3D.userData.PB.assignableComponent
                        // 



                            // for ( let assignable of blockInstance.assignables ) {

                                //     // for every assignable.materialVariantGroups




                                //     let mostCongruentAssignment = 

                                    //     let definingObj3D = obj3D;

                                //     console.log(definingObj3D)

                                //     if ( assignable.positionedMeshGroup) {
                                    //         definingObj3D = definingObj3D.getObjectByName( assignable.positionedMeshGroup.id );
                                    //         console.log(definingObj3D)
                                    //     }
                                //     if ( assignable.positionedMesh) {
                                    //         definingObj3D = definingObj3D.getObjectByName( assignable.positionedMesh.id );
                                    //         console.log(definingObj3D)
                                    //     }
                                //     console.log(definingObj3D)
                                //     this.assignedMaterials.push({
                                    //         ...assignable,
                                    //         material: this.tree.findComponentById( definingObj3D.material.userData.origin )
                                    //     })
                                // }

                        // console.log(this.assignedMaterials)


                        const transformWrapper = new Object3D();

                        transformWrapper.castShadow = true;
                        transformWrapper.receiveShadow = true;

                        transformWrapper.name = `${blockInstance.label}-transform-wrapper`;

                        transformWrapper.add(obj3D);

                        group.add(transformWrapper);

                        group.userData.PB = {
                            origin: this.id
                        };



                        const bbox = this.boundingBoxes.get(blockInstance.id) //blockInstance.boundingBox.clone()

                        //group.add(transformer);
                        //group.userData.origin = this.id;

                        //blockInstance.boundingBox.computeBoundingBox()

                        var boxHelper = new Box3Helper(blockInstance.boundingBox.boundingBox, 0xFF0000)
                        boxHelper.layers.set(bboxLayer)
                        group.add(boxHelper)

                        //vertex positions
                        if( bbox ){
                            const vPosition = bbox.attributes.position;
                            const vector = new Vector3();

                            var boxGeo = new BoxGeometry(0.02, 0.02, 0.02)
                            var material = new MeshBasicMaterial({ color: 0xFF0000 })
                            var mesh = new Mesh(boxGeo, material)
                            mesh.name = "Vertex Helper"


                            for (let i = 0, l = vPosition.count; i < l; i++) {

                                vector.fromBufferAttribute(vPosition, i);
                                //vector.applyMatrix4( child.matrixWorld );
                                const vertex = mesh.clone()

                                this.clonedAssets.push(vertex);

                                vertex.layers.set(bboxLayer)
                                vertex.position.copy(vector)
                                group.add(vertex)
                                //console.log(vector);
                            }
                        }


                    }

                    // console.log(this.assignedMaterials);

                    //compute  total bbox 
                    var boxHelper = new Box3Helper(this.boundingBox.boundingBox, 0xFF0000)
                    boxHelper.layers.set(bboxLayer)
                    group.add(boxHelper)

                    //draw dimenions lines
                    const offset = 0.25


                    //if configuration is not empty then add dimension lines 
                    if (this.boundingBox.boundingBox.max.z !== 0) {

                        var dimLineFront = drawDimensionsLine(group, this.dimensions.dimFront, offset, "front")
                        var dimLineLeft = drawDimensionsLine(group, this.dimensions.dimLeft, offset, "left")
                        //var dimLineHeight = drawDimensionsLine(group, this.dimensions.dimHeight, offset, "height")
                        //var dimLineBack = drawDimensionsLine( configScene, dimensions.dimBack, offset, "back" )
                        //var dimLineRight = drawDimensionsLine( configScene, dimensions.dimRight, offset, "right" )
                    }

                    this._setContent('main', quality, group);

                    const config = this;

                    setTimeout(config.prepareComponentClones.bind(config), 10);

                    break;


                default:

                    this._setContent(part, quality, null);

                    break;

            }

            return this;

        }
}

// mega dirty hack
// allows ComponentTree to use Configurator
// window.Configuration = Configuration;

ComponentTree.Configuration = Configuration;

export { Configuration }