Source: configurator/configurator.js

import { Configuration } from './configuration.js';
import { Reporter } from '../reporter/reporter.js';
import { Package } from '../package/package.js';
import { Group, Vector3, Quaternion } from '../../node_modules/three/build/three.module.js';
import { checkPropTypes } from '../lib.js';
import { Actor } from '../actor/actor.js';
import { BuildableComponent } from '../package/component/buildable_component.js';
import { ConfigurationInfo } from './configuration_info.js';
import { BlockInstance } from './block_instance.js';

/**
 * Configurator
 * @extends Actor
 */
class Configurator extends Actor {

    /**
     * @param {Reporter} reporter
     * @param {Object} settings
     * @param {UUID} [settings.id]
     * @param {Package} settings.pkg
     * @param {Configuration} [settings.configuration]
     * @param {Vector3} [settings.position]
     * @param {Quaternion} [settings.quaternion]
     */

    constructor(reporter, settings) {

        super(reporter, settings);

        checkPropTypes(
            settings,
            {
                pkg: Package
            },
            {
                configuration: Configuration,
                position: Vector3,
                quaternion: Quaternion,
            }
        );

        this.pkg = settings.pkg;

        const initialBodyContent = new Group();
        initialBodyContent.name = "Configurator Group"

        const initialConfig = settings.configuration || new Configuration(
            this._reporter,
            {
                pkg: settings.pkg,
                // info: Configuration.generateInfoComponent(this._reporter, this.pkg),
                name: 'Configurator autogenerated initial config',
                blockInstances: []
            }
        );

        initialConfig
            .build()
            .then(() => {
                this.updateBody(initialConfig.content.main.medium);
            })

        // initialConfig._setContent('main', this._loadingQuality, initialBodyContent);

        this._stateRegister = [
            {
                configuration: initialConfig,
                position: settings.position || new Vector3(),
                quaternion: settings.quaternion || new Quaternion(),
            }
        ];

        this.body.position.copy(this.state.position);
        this.body.quaternion.copy(this.state.quaternion);
    }


    /**
     * @typedef {Object} ConfiguratorState
     * @property {Configuration} configuration
     * @property {Vector3} position
     * @property {Quaternion} quaternion
     * @property {string} [description]
     */





    /** @type {LoadingQuality} */

    _loadingQuality = 'medium';



    /** @param {Object} selectedState */

    async onStateCursorMoved(selectedState) {

        // console.log(selectedState)

        if (selectedState.configuration.status.main[this._loadingQuality] !== 'ready') {
            await selectedState.configuration.build({ part: 'main', quality: this._loadingQuality, highPriority: true });
        }

        return this.updateBody(
            selectedState.configuration.content.main[this._loadingQuality],
            selectedState.quaternion,
            selectedState.position,
            selectedState.modTransform
        );
    }



    get options() {
        const optionObj = { ...this.configuration.options, ...this.configuration.instructions };

        if (this._cursor < this._stateRegister.length - 1) {
            optionObj.redo = this.redo.bind(this)
        }

        if (this._cursor > 0) {
            optionObj.undo = this.undo.bind(this)
        }

        return optionObj;
    }


    /**
     * Current configuration
     * @type {Configuration}
     */

    get configuration() {
        // console.log(this._stateRegister)
        // console.error(this._cursor)
        return this._stateRegister[this._cursor]?.configuration;
    }

    /**
     * Current position
     * @type {Vector3}
     */

    get position() {
        return this._stateRegister[this._cursor].position;
    }

    /**
     * Current quaternion
     * @type {Quaternion}
     */

    get quaternion() {
        return this._stateRegister[this._cursor].quaternion;
    }


    /**
     * @typedef {Object} GenericUpdateResult
     * @property {Configuration} configuration - the current configuration
     * @property {Vector3} position - current body position
     * @property {Quaternion} quaternion - current body quaternion
     * @property {import("./configuration.js").Placement} placement
     */

    /**
     * Applies an insert option to the current configurations
     * @param {import("./configuration.js").InsertOption} option
     * @returns {Promise<GenericUpdateResult>}
     */

    async insert(option) {
        const result = this.configuration.insert(option);
        await this.update(
            result,
            `Inserted ${option.block.label} in step ${this.configuration.step}`
        );
        return { ...this.state };
    }



    /**
     * Applies an insert option to the current configurations
     * @param {import("./configuration.js").ExtendOption} option
     * @returns {Promise<GenericUpdateResult>}
     */

    async extend(option) {
        const result = this.configuration.extend(option);
        await this.update(
            result,
            `Extended with ${option.block.label} in step ${this.configuration.step}`
        );
        return { ...this.state };
    }





    /**
     * @typedef {Object} CutResult
     * @property {ConfiguratorState} main - current configuration wo the cut part
     * @property {ConfiguratorState} cut - new configuration with the cut block instance
     */

    /**
     * Applies an insert option to the current configurations
     * @param {import("./configuration.js").CutOption} option
     * @returns {Promise<CutResult>}
     */

    async cut(option) {

        const baseCorrection = { position: this.state.position, quaternion: this.state.quaternion };

        const result = this.configuration.cut(option);

        const cutCorrection = this.calcPosCorrection(result.cut, this.configuration);

        // console.log(cutCorrection, this.position)

        await this.update(
            result.main,
            `${this.configuration.label} after cutting out ${option.blockInstance.label} in step ${this.configuration.step}`
        );

        return {
            main: this.state,
            cut: {
                configuration: result.cut,
                position: cutCorrection.position.add(baseCorrection.position),
                quaternion: cutCorrection.quaternion.multiply(baseCorrection.quaternion),
                description: `Cut out ${option.blockInstance.label} of ${this.configuration.label} step ${this.configuration.step}`
            }
        };
    }




    /**
     * @typedef {Object} SplitResult
     * @property {ConfiguratorState} cut new configurator with the cut block instance
     * @property {Array<ConfiguratorState>} split new configurator for each of the split sibling chains
     */

    /**
     * Applies an insert option to the current configurations
     * @param {import("./configuration.js").SplitOption} option
     * @returns {Promise<SplitResult>}
     */

    async split(option) {

        const baseCorrection = { position: this.state.position, quaternion: this.state.quaternion };

        const result = this.configuration.split(option);

        const cutCorrection = this.calcPosCorrection(result.cut, this.configuration);

        const splitConfigurations = result.split.map(config => {

                    const chainCorrection = this.calcPosCorrection(config, this.configuration)

                    return {
                        configuration: config,
                        position: chainCorrection.position.add(baseCorrection.position),
                        quaternion: chainCorrection.quaternion.multiply(baseCorrection.quaternion),
                        description: `Sibling chain of splitting ${this.configuration.label} step ${this.configuration.step}`
                    };

                });

        return {
            cut: {
                configuration: result.cut,
                position: cutCorrection.position.add(baseCorrection.position),
                quaternion: cutCorrection.quaternion.multiply(baseCorrection.quaternion),
                description: `Cut out ${option.blockInstance.label} when splitting ${this.configuration.label} step ${this.configuration.step}`
            },
            split: splitConfigurations
        };
    }



    async assignMaterial(option) {
        const baseCorrection = { position: this.state.position, quaternion: this.state.quaternion };

        const newConfig = this.configuration.assignMaterial(option);

        await this.update(
            newConfig,
            `${this.configuration.label} after assigning ${option.material.label} to ${option.blockInstance.label} in step ${this.configuration.step}`
        );

        return newConfig;
    }


    async setTheme(theme) {
        const newConfig = this.configuration.setTheme(theme);

        await this.update(
            newConfig,
            `${this.configuration.label} after setting ${theme.label} in step ${this.configuration.step}`
        );

        return newConfig;
    }

    static get defaultPlacementObject() {
        return {
            position: new Vector3(),
            quaternion: new Quaternion()
        };
    }

    calcPosCorrection(configurationToPlace, anchor) {

        const correction = Configurator.defaultPlacementObject;

        // 1. figure out rotational correction

        /** @type {Array<Array>} */
        const quatCorrs = [[new Quaternion(), 0]];

        for (let newBI of configurationToPlace.blockInstances) {
            if (anchor.placement[newBI.id]) {

                // console.log('Persistent BI:', newBI.id, 'old x', this.configuration.placement[newBI.id].position.x, 'new x', configurationToPlace.placement[newBI.id].position.x)

                const corQuat = new Quaternion()
                    .copy(anchor.placement[newBI.id].quaternion)
                    .conjugate()
                    .multiply(configurationToPlace.placement[newBI.id].quaternion);



                const existingQuatCorr = quatCorrs.find(rotCorrection =>
                    rotCorrection[0].equals(corQuat)
                );

                if (existingQuatCorr) {
                    existingQuatCorr[1] += 1;
                }
                else {
                    quatCorrs.push([corQuat, 1]);
                }

            }
            else {
                // console.log('New BI:', newBI.id)
            }
        }

        // console.log(quatCorrs);

        const quatCorr = quatCorrs.sort((a, b) => b[1] - a[1])[0][0];



        // 2. figure out positional correction with applied rot cor

        const posCorrs = [[new Vector3(), 0]];

        for (let newBI of configurationToPlace.blockInstances) {
            if (anchor.placement[newBI.id]) {

                const posCor = new Vector3().subVectors(
                    anchor.placement[newBI.id].position.clone().applyQuaternion(quatCorr),
                    configurationToPlace.placement[newBI.id].position.clone().applyQuaternion(quatCorr),
                );

                const existingCorrection = posCorrs.find(positionalCorrection =>
                    positionalCorrection[0].equals(posCor)
                );

                if (existingCorrection) {
                    existingCorrection[1] += 1;
                }
                else {
                    posCorrs.push([posCor, 1]);
                }
            }
        }

        // console.log(posCorrs);

        const posCorr = posCorrs.sort((a, b) => b[1] - a[1])[0][0];

        // console.log(posCorr)

        correction.position = posCorr;
        correction.quaternion = quatCorr;


        return correction;
    }



    /**
     * @param {Configuration} newConfiguration 
     * @param {string} description 
     * @param {Object} modTransform 
     * @param {Vector3} modTransform.translation
     * @param {Quaternion} modTransform.quaternion
     * @returns {Promise<Object>}
     */

    async update(newConfiguration, description, modTransform = {}) {

        if (!(newConfiguration instanceof Configuration)) {
            throw new Error('Configurator update parameter is not an instance of Configuration');
        }

        if (newConfiguration.pkg !== this.pkg) {
            throw new Error('Configuration is of a different package');
        }

        const correction = this.calcPosCorrection(newConfiguration, this.configuration);

        let newState = {
            configuration: newConfiguration,
            position: correction.position.add(this.position),
            quaternion: correction.quaternion.multiply(this.quaternion),
            modTransform,
            description,
        };

        const cleanUpState = function({ state, stateRegister, maxStateCount }) {

            console.log('Configurator state clean up')

            // if the configuration in this state is not in the relevant states anywhere, destroy it
            let configurationStillRelevant = false;

            for ( let i = 0 ; i < maxStateCount ; i += 1 ) {
                if ( stateRegister[ i ].configuration === state.configuration ) {
                    configurationStillRelevant = true;
                }
            }

            if ( ! configurationStillRelevant )  {
                console.log('Destroy old configuration')
                state.configuration.destroy();
            }

            state.configuration = null;
            state = null;
        }

        newState.cleanUp = cleanUpState.bind(this);

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

        return this.options;
    }


    /**
     * Merge another configuration into this one
     * @param {Configuration} configuration
     * @returns {Configuration}
     */

    merge(configuration, blockInstance, connector,) {

    }



    async move(newPosition, newQuaternion) {

        await this.addState({
            newState: {
                configuration: this.configuration,
                position: newPosition || this.position,
                quaternion: newQuaternion || this.quaternion,
                description: `Moved configuration`
            },
            moveCursor: true,
        });

        return this.options;
    }


}

export { Configurator }