Source: utilities/calculatePlacement.js

import { Vector3, Quaternion, Euler } from '../../node_modules/three/build/three.module.js';
import { BlockInstance } from "../configurator/block_instance.js";
import { Connection } from "../configurator/connection.js";

/**
 * Find a connection between block instance sets
 * @param {Array<BlockInstance>} blockInstanceSetA 
 * @param {Array<BlockInstance>} blockInstanceSetB 
 * @param {Array<Connection>} connections 
 * @returns {(Object|null)} connectionData
 */

const findConnection = (blockInstanceSetA, blockInstanceSetB, connections) => {
    let blockInA = null;
    let blockInB = null;
    let fromSet = null;
    let toSet = null;

    // find a connection FROM a connectable in set A TO 
    // a connectable in set B

    let connectionToUse = connections.find(connection =>
        blockInstanceSetA.includes(connection.from.connectable)
        &&
        blockInstanceSetB.includes(connection.to.connectable)
    );

    if (connectionToUse) {

        blockInA = connectionToUse.from;
        blockInB = connectionToUse.to;
        fromSet = blockInstanceSetA;
        toSet = blockInstanceSetB;
    }

    // if no from:A-to:B connection was found, find a 
    // a connection from a connectable in set B to a connectable in set A

    else {
        connectionToUse = connections.find(connection =>
            blockInstanceSetB.includes(connection.from.connectable)
            &&
            blockInstanceSetA.includes(connection.to.connectable)
        );

        if (connectionToUse) {
            blockInA = connectionToUse.to;
            blockInB = connectionToUse.from;
            fromSet = blockInstanceSetB;
            toSet = blockInstanceSetA;
        }
    }

    if (!connectionToUse) {
        return null
    }
    else {
        return {
            connection: connectionToUse,
            from: fromSet,
            to: toSet,
            blockInA,
            blockInB
        };
    }
}

/**
 * Calculate the position of the blockInstances and their connectors
 * @param {Configuration} configuration
 * @returns {Object}
 */

const calculatePlacement = (configuration) => {

    // object to store the calculated values in,
    // which will eventually be returned

    const placement = {}

    // check that there are "things to place"

    console.assert(Array.isArray(configuration.blockInstances) && configuration.blockInstances.length > 0, 'No block instances in configuration');

    // The blockInstance's positions will be calculated one by one. Because it is 
    // not a given that their order in the configuration's settings array is the
    // order in which they are connected, the next "calculable" blockInstance
    // is determined for every step, by find an anchor and going working from there
    // for the first step, simply use the first block in the array as anchor



    // set the first block's position and quaternion to default values

    placement[configuration.blockInstances[0].id] = {
        position: new Vector3(),
        quaternion: new Quaternion(),
        type: 'connectable'
    };

    // remove the first block from the "To-Do list" and add to the "done list"

    const uncalculatedBlockInstances = configuration.blockInstances.slice(1, configuration.blockInstances.length);

    const calculatedBlockInstances = [configuration.blockInstances[0]];

    // calculate the placements

    const nrOfPlacementsToCalculate = uncalculatedBlockInstances.length;

    for (let i = 0; i < nrOfPlacementsToCalculate; i += 1) {

        // find a connection between the set of "calculated" blockInstances
        // and the "yet to calculate" ones

        // console.log(
        //     "log!!!!!!",
        //     i,
        //     calculatedBlockInstances.map(cbi => cbi.id),
        //     uncalculatedBlockInstances.map(ucbi => ucbi.block.id),
        //     configuration.connections
        // )

        const connectionData = findConnection(
            calculatedBlockInstances,
            uncalculatedBlockInstances,
            configuration.connections
        );

        if (!connectionData) {
            throw new Error(`Unbuildable configuration. Missing connection between placed and unplaced block instances.`);
        }

        const connection = connectionData.connection;
        const anchor = connectionData.blockInA;
        const target = connectionData.blockInB;

        // sanity checking

        console.assert(anchor.connector.quaternion instanceof Quaternion);
        console.assert(connection.quaternion instanceof Quaternion);
        console.assert(connection.inverseQuaternion instanceof Quaternion);
        console.assert(target.connector.quaternion instanceof Quaternion);

        // determine the vector from the anchoring blockInstance to the connection point
        // by taking the connector position and rotating it according to the
        // blockInstance's rotation

        const anchorToConnectionVector = anchor.connector.position.clone();
        anchorToConnectionVector.applyQuaternion(placement[anchor.connectable.id].quaternion);

        // console.debug('anchorToConnectionVector', anchorToConnectionVector);

        // Determine the vector from the connection point to the target position
        // by taking the inverse vector to the target connector and rotating it
        // according to (1) the anchor connector rotation, (2) the connection rotation and
        // (3) the inverse target connector rotation

        const connectionToTargetVector = target.connector.position.clone().negate();

        const connectionToTargetQuaternion = new Quaternion();

        connectionToTargetQuaternion.copy(placement[anchor.connectable.id].quaternion);

        // console.debug('step 0', new Euler().setFromQuaternion(connectionToTargetQuaternion))

        // connection to target quaternion step 1

        connectionToTargetQuaternion.multiply(anchor.connector.quaternion);

        // console.debug('step 1', new Euler().setFromQuaternion(connectionToTargetQuaternion))

        // connection to target quaternion step 2
        // the connection rotaion is specified as a quaternion 
        // between the from-connector to the to-connector, so if the 
        // anchor connector is the "to-connector" of the connection
        // use the inverse quaternion

        if (connectionData.from === calculatedBlockInstances) {
            connectionToTargetQuaternion.multiply(connection.quaternion);
        }
        else {
            connectionToTargetQuaternion.multiply(connection.inverseQuaternion);
        }

        // console.debug('step 2', new Euler().setFromQuaternion(connectionToTargetQuaternion))

        connectionToTargetQuaternion.multiply(Connection.baseQuaternion);

        // console.debug('step 3', new Euler().setFromQuaternion(connectionToTargetQuaternion))

        // connection-to-target-quaternion step 3

        connectionToTargetQuaternion.multiply(
            target.connector.quaternion.clone().invert() 
        );

        // console.debug('step 4', new Euler().setFromQuaternion(connectionToTargetQuaternion))

        // console.debug('connectionToTargetQuaternion', connectionToTargetQuaternion);

        // apply the connection-to-target-quaternion to the connection-to-target-vector
        // the vector now points to the correct position from the connection point

        connectionToTargetVector.applyQuaternion(connectionToTargetQuaternion);

        // console.debug( 'connectionToTargetVector', connectionToTargetVector)

        // determine the final position of the target by taking the position
        // of the anchor as the starting position 

        const targetPosition = placement[anchor.connectable.id].position.clone();

        // add the vector to the connection point

        targetPosition.add(
            anchorToConnectionVector
        );

        // add the vector from the connection point to the target position

        targetPosition.add(
            connectionToTargetVector
        );

        // determine the final quaternion by taking the anchor's quaternion 
        // as starting point

        // const targetQuaternion = new Quaternion();
        // targetQuaternion.multiply(placement[anchor.connectable.id].quaternion);

        // multiply with the quaternion from the anchor's rotation
        // to the target rotation

        // targetQuaternion.multiply(connectionToTargetQuaternion);

        // add the calculated values to the placement register to return afterwards
        // include debug data for testing purposes

        placement[target.connectable.id] = {
            position: targetPosition,
            quaternion: connectionToTargetQuaternion,
            debug: {
                anchorTo: anchor,
                connection,
                anchorToConnectionVector,
                connectionToTargetVector,
            },
            type: 'connectable'
        };

        const connectionPosition = placement[anchor.connectable.id].position.clone().add(anchorToConnectionVector);

        placement[connection.id] = {
            position: connectionPosition,
            // quaternion: connectionToTargetQuaternion,   // connection has two quaternions, before and after..
            debug: {
                anchorTo: anchor,
                connection,
                anchorToConnectionVector,
                connectionToTargetVector,
            },
            type: 'connection'
        };

        // do bookkeeping - remove the calculated block from the "To-Do" list
        // and add it to the "done" list

        calculatedBlockInstances.push(
            ...uncalculatedBlockInstances.splice(
                uncalculatedBlockInstances.indexOf(target.connectable),
                1
            )
        );
    }

    return placement
}






// const group = new Group();
//                 group.name = `Build of ${this.label}`;

//                 // Initialize the configuration build by placing the first blockInstance
//                 // in the group. This could have been any blockInstance, so the first one is fine.

//                 console.assert(this.blockInstances[0].content.main[quality] instanceof Object3D);


//                 //Add Boundingbox tofirst block
//                 const startBlock = this.blockInstances[0].content.main[quality]


//                 const startBlockBBox = this.blockInstances[0].block.boundingBox
//                 console.log( startBlockBBox )
//                 //startBlock.add( startBlockBBox )

//                 if( startBlockBBox instanceof Mesh ){
//                     //const boundingBoxHelper = new Box3Helper( startBlockBBox, 0xff0000 );
//                     //boundingBoxHelper.layers.set(1)
//                     startBlock.add( startBlockBBox )
//                 }   

//                 group.add( startBlock );
//                 console.log( startBlock )


//                 // 
//                 // Algo:
//                 // 1. Place first block
//                 // 2. Find a connection between a block that has been "placed" and one that is "unplaced"
//                 // 3. Place that block
//                 // 4. Go to 2.

//                 const placedBlockInstances = [this.blockInstances[0]];

//                 //console.log( this.blockInstances.slice( 1, this.blockInstances.length ) )

//                 const unplacedBlockInstances = this.blockInstances.slice(1, this.blockInstances.length)

//                 const nrOfBlocksToPlace = unplacedBlockInstances.length;

//                 // console.log( 'placedBlockInstances', placedBlockInstances )
//                 // console.log( 'unplacedBlockInstances', unplacedBlockInstances )

//                 for (let i = 0; i < nrOfBlocksToPlace; i += 1) {

//                     // console.log( 'Find next block to place in unplacedBlockInstances', unplacedBlockInstances );

//                     let placedConnectable = null;
//                     let unplacedConnectable = null;

//                     //FIND CONNECTION TO USE

//                     //get the connections from placed vs not placed blocks
//                     let connectionToUse = this.connections.find(connection =>
//                         placedBlockInstances.includes(connection.from.connectable)
//                         &&
//                         unplacedBlockInstances.includes(connection.to.connectable)
//                     );

//                     //get from -> to connectables
//                     if (connectionToUse) {

//                         unplacedConnectable = connectionToUse.from;
//                         placedConnectable = connectionToUse.to;
//                     }

//                     //if not found reverse and get the connecions from not placed vs placed blocks
//                     else {
//                         connectionToUse = this.connections.find(connection =>
//                             unplacedBlockInstances.includes(connection.from.connectable)
//                             &&
//                             placedBlockInstances.includes(connection.to.connectable)

//                         );
//                     }

//                     //get from -> to connectables from reverse
//                     if (connectionToUse) {
//                         unplacedConnectable = connectionToUse.to;     // this needs to match the connection distribution!!
//                         placedConnectable = connectionToUse.from;
//                     }

//                     //if no connections found then give error
//                     else {
//                         // console.error({ placedBlockInstances, unplacedBlockInstances, connections: this.connections });
//                         throw new Error(`Unbuildable configuration. Missing connection between placed and unplaced block instances.`);
//                     }

//                     //get the blockInstance of the unplaced block (unplacedConnectable)
//                     const object3DToPlace = unplacedConnectable.connectable.content.main[quality];

//                     const object3DToPlaceBBox = unplacedConnectable.connectable.block.boundingBox
//                     if( object3DToPlaceBBox instanceof Box3 ){
//                         const boundingBoxHelper = new Box3Helper( object3DToPlaceBBox, 0xff0000 );
//                         boundingBoxHelper.layers.set(1)
//                         object3DToPlace.add( boundingBoxHelper )
//                     }

//                     console.log( unplacedConnectable )

//                     //chek if the block is a Object3D
//                     console.assert(object3DToPlace instanceof Object3D);

//                     //add the block to the config group
//                     group.add(object3DToPlace);


//                     //cube helper
//                     const geometry = new BoxGeometry( 0.3, 0.3, 0.3 );
//                     const material = new MeshBasicMaterial( {color: 0x00ff00} );
//                     const cubeHelper = new Mesh( geometry, material );
//                     //group.add( cube )


//                     //--FROM BLOCK
//                     const placedBlockPosition = placedConnectable.connectable.content.main[quality].position.clone();
//                     const placedBlockQuaternion = placedConnectable.connectable.content.main[quality].quaternion.clone();

//                     //--FROM CONNECTOR
//                     //get the position and quaternion of the placed block connector
//                     const originalConnectorPosition = placedConnectable.connector.position.clone();
//                     const originalConnectorQuaternion = placedConnectable.connector.quaternion.clone();

//                     //rotate & translate the placed connector position & quaternion
//                     const originalConnectorRotated = originalConnectorPosition.applyQuaternion( placedBlockQuaternion )         //rotate position vector
//                     const placedFromConnectorPosition = originalConnectorRotated.add( placedBlockPosition )                     //translate position
//                     const placedFromConnectorQuaternion = originalConnectorQuaternion.multiply( placedBlockQuaternion )         //rotate quaternion
//                     const inverseFromQuaternion = placedFromConnectorQuaternion.multiply( { _w:0, _x:0, _y:1, _z:0 } )          //rotate quaternion 180 degrees

//                     //HALFWAY POSITION & ROTATION
//                     //cubeHelper.position.copy( placedFromConnectorPosition )
//                     object3DToPlace.position.copy( placedFromConnectorPosition ); //endPosition
//                     object3DToPlace.quaternion.multiply( placedFromConnectorQuaternion );  //endInverseQuaternion

//                     //--TO CONNECTOR
//                     //get the local postion and orientation of the unplaced connector 
//                     const unplacedConnectorPosition = unplacedConnectable.connector.position.clone();                              //get the toConnector vector 
//                     const invertedConnectorPosition = unplacedConnectorPosition.negate()                                           //invert toConnector vector
//                     const rotatedToConnectorPosition =  invertedConnectorPosition.applyQuaternion( inverseFromQuaternion )         //apply 180 degrees rotation to vector

//                     //END POSITION & ROTATION
//                     //cubeHelper.position.add( rotatedToConnectorPosition )
//                     object3DToPlace.position.add( rotatedToConnectorPosition ); //endPosition

//                     placedBlockInstances.push(
//                         ...unplacedBlockInstances.splice(
//                             unplacedBlockInstances.indexOf(unplacedConnectable.connectable),
//                             1
//                         )
//                     );
//                 }

export { calculatePlacement, findConnection }