import { pick, UUIDRegex, URLRegex, unique, getAssigningComponent } from '../../lib.js';
import { Vector3, Quaternion, Group, ArrowHelper, AxesHelper, BufferGeometry, BoxHelper, Matrix4, BufferAttribute, Math, Box3, Box3Helper, BoxGeometry, MeshBasicMaterial, Mesh } from '../../../node_modules/three/build/three.module.js';
import { checkPropTypes } from '../../lib.js';
import { Reporter } from '../../reporter/reporter.js';
import { Component } from '../../component/component.js';
import { Connector } from '../connector/connector.js';
import { WrappedImage } from '../image/wrapped_image.js';
import { BuildableComponent } from '../component/buildable_component.js';
import { PositionedMesh } from '../mesh/positioned_mesh.js';
import { PositionedMeshGroup } from '../mesh/positioned_mesh_group.js';
import { Project } from '../../project.js';
import { BlockCategory } from './block_category.js';
import { Theme } from '../theme/theme.js';
import { PositionedComponent } from '../component/positioned_component.js';
import { WrappedMesh } from '../mesh/wrapped_mesh.js';
import { MaterialSet } from '../material/material_set.js';
/**
* @typedef {Object} SubAssignable
* @property {PositionedMesh} [positionedMesh]
* @property {PositionedMeshGroup} [positionedMeshGroup]
* @property {Array<MaterialSet>} [materialVariantGroups]
* @property {BuildableComponent} [assigningComponent]
*/
/**
* Unit of geometric configuration and pricing
* Blocks can be connected to other blocks through connectors.
*/
class Block extends BuildableComponent {
/**
* @param {Reporter} reporter
* @param {Object} settings
* @param {UUID} [settings.id]
* @param {string} [settings.name]
* @param {Object} [settings.userData]
* @param {Array<BlockCategory>} [settings.categories]
* @param {Theme} [settings.theme]
* @param {Object} [settings.dimensions]
* @param {Object} [settings.dimensionsCenter]
* @param {WrappedImage} [settings.dimensionsImage]
* @param {WrappedImage} [settings.thumbnail]
* @param {Array<Connector>} [settings.connectors]
* @param {Array<PositionedMesh>} [settings.positionedMeshes]
* @param {Array<PositionedMeshGroup>} [settings.positionedMeshGroups]
* @param {Boolean} [settings.assignable = true]
*/
constructor(reporter, settings) {
// set defaults
settings.assignable = typeof (settings.assignable) === 'boolean' ? settings.assignable : true;
settings.dimensions = settings.dimensions !== undefined ? settings.dimensions : new Vector3(1,1,1);
settings.dimensionsCenter = settings.dimensionsCenter !== undefined ? settings.dimensionsCenter : new Vector3(0.5,0.5,0.5);
super(
reporter,
settings,
{
parse: {
positionedMeshes: 'integral',
positionedMeshGroups: 'integral',
connectors: 'integral',
categories: 'integral',
}
}
);
// mf typescript
if (!this.connectors) {
/** @type {Array<Connector>} */
this.connectors = [];
}
checkPropTypes(
settings,
{},
{
theme: Theme,
connectors: val =>
Array.isArray(val) &&
val.every(cn => cn instanceof Connector),
positionedMeshes: val =>
Array.isArray(val) &&
val.every(m => m instanceof PositionedMesh),
positionedMeshGroups: val =>
Array.isArray(val) &&
val.every(em => em instanceof PositionedMeshGroup),
categories: val =>
Array.isArray(val) &&
val.every(em => em instanceof BlockCategory),
}
);
// theme sanity checking
// block can not have BOTH a theme and themed connectors
if (settings.theme) {
const themedConnector = (settings.connectors || []).find(cntr => cntr.themeGroup);
if (themedConnector) {
throw new Error(`${this.label} has theme "${settings.theme.name}" AND ${themedConnector.label} with theme group "${themedConnector.themeGroup.name}". This is not allowed.`);
}
}
// set block field on connectors to this object
for (let connector of settings.connectors || []) {
connector.block = this;
}
// positioned meshes can be be re-used,
this.distinctPositionedMeshes = this.allDependencies.filter(dep => dep instanceof PositionedMesh).filter(unique);
// meshes can be re-used
this.distinctMeshes = this.distinctPositionedMeshes.map(pm => pm.component).filter(unique);
this.distinctMVGs = [];
this.distinctMeshesPerMVG = {};
for (let mesh of this.distinctMeshes) {
// only meshes can have a material variant group
if (mesh.materialVariantGroup) {
if (!this.distinctMVGs.includes(mesh.materialVariantGroup)) {
this.distinctMVGs.push(mesh.materialVariantGroup);
}
this.distinctMeshesPerMVG[mesh.materialVariantGroup.id] = [
mesh,
...(this.distinctMeshesPerMVG[mesh.materialVariantGroup.id] || [])
];
}
}
// no need to filter on unique, because only positioned components can be assignable
// (beside the block itself) and these are already unique
this.subAssignables = (settings.positionedMeshes || [])
.filter(pm => pm.assignable == true)
.map(apm => ({ positionedMesh: apm }));
for (let posMeshGroup of settings.positionedMeshGroups || []) {
// add the positioned mesh group to the list of assignable if appropriate
if (posMeshGroup.assignable === true) {
this.subAssignables.push({
positionedMeshGroup: posMeshGroup
});
}
// add its assignables dependencies icw the group
this.subAssignables.push(
...posMeshGroup.assignableDependencies.map(assDep => ({
positionedMeshGroup: posMeshGroup,
positionedMesh: assDep
}))
)
}
for ( let subAssignable of this.subAssignables ) {
subAssignable.assigningComponent = getAssigningComponent(subAssignable);
subAssignable.materialVariantGroups = subAssignable.assigningComponent.applicableMaterialVariantGroups;
subAssignable.id = `B:${this.slug}`;
subAssignable.name = this.name || this.slug;
if ( subAssignable.positionedMeshGroup) {
subAssignable.name += ' ' + subAssignable.positionedMeshGroup.slug;
subAssignable.id = `-MG:${subAssignable.positionedMeshGroup.slug}`;
}
if ( subAssignable.positionedMesh) {
subAssignable.name += ' ' + subAssignable.positionedMesh.name || subAssignable.positionedMesh.slug;
subAssignable.id = `-M:${subAssignable.positionedMeshGroup.slug}`;
}
}
//setup min & max
//const boxMin = new Vector3( 0,0,0)
const boxMin = new Vector3( -this.settings.dimensions.x/2, -this.settings.dimensions.y/2, -this.settings.dimensions.z/2 )
const boxMax = new Vector3( this.settings.dimensions.x/2, this.settings.dimensions.y/2, this.settings.dimensions.z/2 )
//Math.round( line.distance() * 100 )
//create boundingbox
const boundingBox = new Box3( boxMin, boxMax )
//create vertices
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
const boundingBoxGeometry = new BufferGeometry()
boundingBoxGeometry.name = "boundingBox";
boundingBoxGeometry.visible = false;
boundingBoxGeometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );
//console.log( boundingBoxGeometry )
const matrix = new Matrix4();
const qat = new Quaternion();
const scale = new Vector3( 1, 1, 1)
matrix.compose( this.settings.dimensionsCenter, qat, scale )
//console.log( matrix )
boundingBoxGeometry.applyMatrix4( matrix )
//boundingBoxGeometry.computeBoundingBox()
//boundingBoxGeometry.layers.set( Project.layerMap.boundingBox );
this.boundingBox = boundingBoxGeometry
//console.log( this.boundingBox.boundingBox )
}
static _exportName = {
singular: 'block',
plural: 'blocks'
};
/**
* All positioned meshes that are (directly and indirectly) referenced in this block
* @type {Array<PositionedMesh>}
*/
distinctPositionedMeshes;
/**
* All (wrapped) meshes that are (indirectly) referenced in this block
* @type {Array<WrappedMesh>}
*/
distinctMeshes;
/**
* Material variant groups that are referenced by at least one of
* the meshes in this block
* @type {Array<MaterialSet>}
*/
distinctMVGs;
/** @type {Object<UUID,Array<PositionedMesh>>} */
distinctMeshesPerMVG;
/**
* The assignables this block offers, includes the block itself if it is assignable
* @type {Array<SubAssignable>}
*/
subAssignables;
_onTreeSet() {
// if (this._tree.materialSets) {
// for (let materialSet of this._tree.materialSets) {
// // the meshes in the blockInstance that will go in the Configuration
// // are all wrapped in a PositionedMesh class and will have a reference
// // to the original positionedMesh object in their userData
// this.meshesPerMaterialSet[materialSet] = this.allPositionedMeshes.filter(
// posMesh => posMesh.component.materialVariantGroup === materialSet
// );
// }
// }
}
/**
* Build
* @param {ComponentPart} part
* @param {LoadingQuality} quality
* @param {Object<string,Component>} dependencies
* @returns {Promise<Block>}
*/
async _build(part, quality, dependencies) {
switch (part) {
case 'UI':
//console.log( 'build 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':
/** @type {Group} */
const block = new Group();
block.castShadow = true;
block.receiveShadow = true;
//console.log( this.boundingBox.boundingBox )
//const boxHelper = new Box3Helper( this.boundingBox.boundingBox, 0xffff00 ) //-> helpers kunnen nog nie gecloned worden!
//block.add( boxHelper )
// console.log( this.settings )
//HELPER - BASE POINT
const axesHelper = new AxesHelper(5);
//HELPER - ARROW -> CONNECTOR
const connectorGroup = new Group()
connectorGroup.name = "connectors"
const dir = new Vector3(0, 0, 0.1);
dir.normalize(); //normalize the direction vector (convert to vector of length 1)
const origin = new Vector3(0, 0, 0); // origin of the arrowHelper (not able to set oafter creation -> use position instead)
const length = 0.15; // length of the arrowHelper
const hex = 0xff0000; // color of the arrowHelper
const headLength = 0.05 // The length of the head of the arrow. Default is 0.2 * length.
const headWidth = 0.05 // The width of the head of the arrow. Default is 0.2 * headLength.
const arrowHelper = new ArrowHelper(dir, origin, length, hex, 0.05, 0.05); // arrowhelper template
//HELPER - ARROW -> place helpers in block group
// for (var i = 0; i < this.settings.connectors.length; i++) {
// //get the connector data
// //console.log( this.settings)
// const conn = this.settings.connectors[i].content.main[quality] //get the connector
// //console.log( conn )
// const newDir = new Vector3(0, 0, 2); //create new vector, deze moet groter zijn dan 1! zit anders een bug in dat die de vector niet negatief kan roteren!
// newDir.applyQuaternion(conn.quaternion) //apply connector rotation to vector
// //console.log( newDir )
// const newArrowHelper = arrowHelper.clone() //create copy of the arrow helper template
// newArrowHelper.position.copy(conn.position); //copy the position of the connector to the arrowhelper
// newArrowHelper.setDirection(newDir)
// //newArrowHelper.layers.set( layerMap.connectorHelpers )
// connectorGroup.add(newArrowHelper)
// //connectorGroup.layers.set( layerMap.connectorHelpers )
// //block.add(connectorGroup)
// }
// clone and add the MeshGroups to the block
// and position correcty
if (this._settings.positionedMeshGroups) {
/** @type {Array<PositionedMeshGroup>} */
const positionedMeshGroups = Object.values(this._settings.positionedMeshGroups || {});
if (this._debug) {
console.log('PosMeshGroups groups', positionedMeshGroups);
}
for (let positionedMeshGroup of positionedMeshGroups) {
/** @type {Group} */
const positionedMeshGroupClone = positionedMeshGroup.content.main[quality].clone();
block.add(positionedMeshGroupClone);
}
}
// clone and add the (Wrapped)Meshes to the group
// and position correcty
if (this._settings.positionedMeshes) {
/** @type {Array<PositionedMesh>} */
const positionedMeshes = Object.values(this._settings.positionedMeshes);
// console.log( this._settings.positionedMeshes )
if (this._debug) {
console.log('Positioned meshed', positionedMeshes);
}
for (let positionedMesh of positionedMeshes) {
/** @type {THREE.Mesh} */
const positionedMeshClone = positionedMesh.content.main[quality].clone();
positionedMeshClone.castShadow = true;
positionedMeshClone.receiveShadow = true;
block.add(positionedMeshClone);
}
}
// export the block and its connectors
const content = {
block,
connectors: this._settings.connectors || []
}
// console.log( content)
this._setContent('main', quality, content);
break;
case 'specs':
//console.log( 'build specs')
if (this._settings.dimensionsImage) {
this._setContent('specs', quality, this._settings.dimensionsImage.content.main[quality]);
}
else {
this._setContent('specs', quality, Project.defaultImages.missing.cloneNode(true));
}
break;
default:
this._setContent(part, quality, null);
break;
}
return this;
}
}
export { Block };