import { Package } from './package/package.js';
import { Timer } from './timer.js';
import { Reporter } from './reporter/reporter.js';
import { Actor } from './actor/actor.js';
import { View } from './view/view.js';
import { Block } from './package/block/block.js';
import { DefaultView } from './view/default_view.js';
import { InformationSource } from './reporter/information_source.js';
import { Configurator } from './configurator/configurator.js';
import { LoadingBase } from './package/loader/loading_base.js';
import { BlockInstance } from './configurator/block_instance.js';
import { ComponentLoader } from './package/loader/component_loader.js';
import { Scene, WebGLRenderer, Quaternion, Euler, Vector3 } from '../node_modules/three/build/three.module.js';
import { makeImage } from './lib.js';
import defaultRendererSettings from './default_settings/renderer.js';
import { SingleBlockInstance } from './actor/single_block_instance.js';
import { ServerConnection } from './server/server_connection.js';
import { StateTracker } from './state_tracker.js';
import { Transform } from './transform/transform.js';
import { pick } from "./lib.js"
import { Component } from './component/component.js';
import { Theme } from './package/theme/theme.js';
import { Configuration } from './configurator/configuration.js';
// App has 1 Scene
// App has n View
// App has n Packages
// App has n Configurator
// Configurator = Package => Config => Object3D
// App moet projecten inladen.
// Er moet toch iets van een base.productbuilder.com komen, die aan loaders kan vertellen welke packages en materiallibs waar staan.
// Wellicht zou dit met git hooks kunnen werken: package is een repo, repo ergens planten -> meldt zich aan bij packagebase.
/**
* Base class
* @constructor
*
* @mermaid
* graph TD;
* ComponentTree-->Package;
* ComponentTree-->Configuration;
* GeometryFile-->Geometry;
* Geometry-->Mesh;
* WrappedImage-->WrappedTexture;
* WrappedTexture-->WrappedMaterial;
* WrappedMaterial-->MaterialCategory;
* WrappedMaterial-->MaterialSet;
* MaterialCategory-->MaterialSet;
* WrappedMaterial-->Mesh;
* MaterialSet-->Mesh;
* Mesh-->MeshGroup;
* Mesh-->PositionedMesh;
* MeshGroup-->PositionedMeshGroup;
* PositionedMesh-->Block;
* PositionedMeshGroup-->Block;
* ConnectorType-->Connector;
* Connector-->Block;
* Block-->Package;
* Connector-->Configuration;
* Block-->Configuration;
* Package-->Configuration;
* Configuration-->Project;
*/
/**
* @class Project
* An actor should not be removed during runtime, or undo/redo will mess up
*/
class Project extends StateTracker {
/**
* @param {Reporter} reporter
* @param {Object} settings
* @param {URL} [settings.server]
* @param {Object} [settings.rendererSettings]
* @param {number} [settings.reconnectTime = 5]
*/
constructor(reporter, settings = {}) {
super(reporter, settings);
window.onerror = (message, source, lineno, colno, error) => {
this.report({
msg: 'Uncaught error: ' + message,
level: 'error'
});
console.error(error);
}
Object.defineProperty(window, 'uuid', {
get: InformationSource.uuid
});
const project = this;
this.exportConfig = function () {
var project = this;
var config;
window.exportConfig = function () {
config = JSON.stringify(pick(
['info', 'blockInstances', 'connections', 'materialAssignments', 'themes'],
project.configurators[0].configuration.toJSON()
)
)
console.log(config)
}
}
this.exportConfig()
this.rendererSettings = {
...defaultRendererSettings,
...(settings.rendererSettings || {})
};
//default renderer for the perspective camera
this.renderer = new WebGLRenderer(this.rendererSettings);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0x000000, 0);
//renderer for the orthographic camera
this.rendererOrtho = new WebGLRenderer(this.rendererSettings);
this.rendererOrtho.setPixelRatio(window.devicePixelRatio);
Project.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy()
this.timer = new Timer(this._reporter);
this.timer.on(
'update',
async () => {
if (this.bodyChangeHandlePromise === null) {
// console.log('update views')
for (let view of this.views) {
view.update();
}
}
else {
// for (let transform of Object.values(this.transforms)) {
// transform.relinkObject3Ds();
// }
for (let view of this.views) {
const projectData = this.configurators.map(configurator => ({
boundingBox: configurator.configuration.boundingBox,
configurationId: configurator.configuration.id,
id: configurator.id
}));
view.hideDimensions()
view.onSceneUpdate(projectData); // also updates view
}
let stateChange = false;
let newState = {};
for (let actor of this.actors) {
if (actor.visible === false) {
continue;
}
newState[actor.slug] = actor.cursor;
if (this.state?.[actor.slug] === undefined || this.state[actor.slug] !== newState[actor.slug]) {
// console.log('state change', actor.slug, this.state?.[actor.slug], '=>', newState[actor.slug]);
stateChange = true;
}
}
if (stateChange) {
// console.log('Adding new state')
await this.addState({
newState,
moveCursor: 'silent'
});
// console.log(this._stateRegister);
// console.log('Added new state')
}
this.bodyChangeHandlePromiseResolver();
this.bodyChangeHandlePromise = null;
this.bodyChangeHandlePromiseResolver = null;
}
}
);
if (settings.server) {
this.server = new ServerConnection(
reporter,
{
url: settings.server || new URL('wss://backend.productbuilder.nl'),
reconnectTime: settings.reconnectTime || 5
}
);
}
// this.addTransformClass(DefaultSelectionTransform);
// this.addTransformClass(ColorTransform);
// StateTracker must have an initial state
// this._stateRegister = [
// // {}
// ];
const autosaveData = this.loadFromLocalStorage('autosave');
if (autosaveData) {
this.autosave = {
age: ( new Date().getTime() - autosaveData.meta.time ) / 1000,
data: autosaveData,
restore: async () => {
console.log('restoring autosave');
return project.buildProjectFromExport(autosaveData);
}
};
// console.log('autosave found', autosaveData.meta.time);
}
this.reset();
}
/**
* Map of 3D content type to THREE layer to keep everything nicely (and consistently) separated
* @type {Object.<string,number>}
*/
static layerMap = {
grid: 1,
boundingBoxes: 2,
dimLines: 3,
connectorHelpers: 4,
lightHelpers: 5,
visibleActors: 6,
hiddenActors: 7
};
static maxAnisotropy
/**
* Set of quaternions that are used often
* @type {Object<string,Quaternion>}
*/
static standardQuaternions = {
'+x': new Quaternion().setFromEuler(new Euler(Math.PI, 0, 0)),
'-x': new Quaternion().setFromEuler(new Euler(-Math.PI, 0, 0)),
'+y': new Quaternion().setFromEuler(new Euler(0, Math.PI, 0)),
'-y': new Quaternion().setFromEuler(new Euler(0, - Math.PI, 0)),
'+z': new Quaternion().setFromEuler(new Euler(0, 0, Math.PI)),
'-z': new Quaternion().setFromEuler(new Euler(0, 0, -Math.PI)),
};
/**
* @type {Object<string,HTMLImageElement>}
*/
static defaultImages = {
missing: makeImage(128, 128, 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAEzklEQVR4nO3dsW7bSBCAYRVJe5DTOEiRGK4CuNEj6BHYEJxZN2pOCFKm8iPcA6W4IkHUB4HdnZtAblLHb5Brdn3rxeYkWTtLkfo/wI2L3SVnSJGzS3IyAQAAAAAAAAAAAAAAAAAAAAAAAEoSkYWqXjdNM+17LE/VNM1UVa9FZNH3WAbFB/+X/xtkEoTgh+0gCbaUBH+QSZAGnyTYUib4d0NLgkzw70iCLYjIWbqjmqaZishN9P9V3+PcRFVX0Tbc+G1YJNt21vc4D1LYUfFREpJARO6dc7Meh7cV59xMRO5D8MP/c9uGjNzR0TTNdAjBD5xzs9zPFUc+AHAqzDiafWJxMdR13QdVPS3V3ib+juV9wfaO4wLR4nZIVa98e7c1kkBEzkRk7cf/rkR7R1EnyAR/sW+by+XyeVJ0+ccyCeLg+234Op/PnxVot/i+OSiWG+icO0mS4NY596ZU+0Em+N/atn1RsP3xJkFczRORdemSbiYJ7komgXXwJ5OHglfcx03J9nuVKekWr+tbJUHbti9rBD+ZObwZwrzHTmongYj8cM6d79umiLxS1e/hGoPg7yFOAqvavnPuREQ+qepFwTbPVfVz27YvS7UZtT0TkfvRBz/wGb8aUm3fmnNupqqr0QcfAIANLH8Lc6uNa6ze5fd9SyLyl1VdX1Wb9DY0s4avKd1vVEi6Kt32qIjIn3FJt/RtV64WYX0PnlYRu65rS7Y/Kqp66o/+hyQofSbIJIFZ8GtUEUenjySwCr6fnXyYqST4W/JJcF0jCSoGv3gVcdQykzt/l+4jXASWbldEPlpeyxyNkASlJnZqcc6di8gPjvwCfBIUm9ipRVXfEnwAAOyoatNHnd2XkIuXjbGDaCVt1XcHxPMHo1q9OzTWawxzcmv4rPvEb9RYaJr2dzQLOIeiVhIQ/ANVKzC1zzbYQu2jkiQ4MNZTujm5JLDuE78RPVdf9fc4ecBlUatfZFAIAgBgnPz6urd9j2NXqnrhnDvpexyDFhZXDnhJ2DVJ8EQ++LfRff7H0n1YLQrV6IXRJMET1FhWHVURLd5U8iZ5toEk2Fbl4JuVdDMPuJAEm7Rt+6KH4JMEh6Lrujb6zV+3bfu6ZPu5ySPryZ3MtcyiZPujo6pXIrK2eHmyRo+Hh/mDzORO8fJudCbg8fBtWL8gIp08ip4TXFj1e3l5+YdV2wAADIH/vf3CiyL/498W+mX0y8uS27CfFkngb7s+l5w7UNULEflkcQ/v3xL6c/RrDGss4EyqiN9F5NW+bUYTOyaFnKNYaFop+I+qiL6QtHcV0df1H33CliTYUbJxdxbBF5FvcfBLFpJqJUHaR8n2e2X5ORTr4AfWSTDqT8ZMJjYbOJ/Pn4nIV+vgB7kkWC6Xz/dtd/TBDyw+Gyci72oEP0jm+veu7R/NZ+MCMfhIoqq+rxH8qL/Trus+lGrPYp8ctJrBGgr2CQBkT4VN00yHNHfgnJvlah2c5jfIXQxFVUSTuYPSotr+o2re0V3o7Sp3O5RZwLnqe5ybaPJMgC/zFr/9HaXMjoo/tjCId/JkvkWwThO77zEetDQJhhT84H++SrLoe2yDECfB0IIfZM4Ei77HNCi51btDU2O1MQAAAAAAAAAAAAAAAAAAAAAAAI7UvxmoXFiIe/GjAAAAAElFTkSuQmCC')
};
/**
* @type {string}
*/
static basis = '+X+Y+Z';
/** @type {(Promise|null)} */
bodyChangeHandlePromise = null;
bodyChangeHandlePromiseResolver = null;
/** @type {Timer} */
timer;
/**
* @type {Scene}
*/
scene;
/**
* @type {WebGLRenderer}
*/
renderer;
/**
* @type {ServerConnection}
*/
server;
/**
* List of packages
* @type {Array<Package>}
*/
pkgs;
/**
* @param {URL} packageURL
*/
async addPackage(packageURL) {
console.assert(packageURL instanceof URL);
const indexURL = `${packageURL.href}${packageURL.href.substr(-1) !== '/' ? '/' : ''}index.json?r=${Math.random()}`;
this.report({ msg: `Loading index from ${indexURL}` });
const response = await fetch(indexURL);
//console.log( response )
if (!response.ok) {
console.warn(response);
throw new Error('Load package failed');
}
const packageJSON = await response.json();
// console.log( packageJSON )
const packageURLBase = new LoadingBase(
this._reporter,
{ name: 'Package URL base', url: packageURL }
);
const loader = new ComponentLoader(this._reporter, { name: 'PackageURLLoader', bases: [packageURLBase] });
const pkg = await Package.createFromJSON(
packageJSON,
this._reporter,
null,
{
loader,
renderer: this.renderer
});
Object.freeze(pkg)
// const pkg = new Package(
// this._reporter,
// {},
// {
// loader,
// renderer: this.renderer
// }
// );
// await pkg.importJSON(packageJSON);
this._addPackageObject(pkg);
this.report({ msg: `Loaded ${pkg.label}`, level: 'notice' });
return pkg;
}
/**
* Add a new package to the internal register
* @param {Package} pkg
*/
_addPackageObject(pkg) {
this.report({ msg: `Adding ${pkg.label}`, level: 'notice' });
this.pkgs.push(pkg);
}
get actors() {
return [...this.configurators, ...this.singleBlockInstances];
}
/**
* @param {Actor} actor
*/
addActor(actor, list, overwritePreviousState = false) {
console.assert(actor instanceof Actor);
this.report({ msg: `Adding ${actor.label}`, level: 'notice' });
list.push(actor);
this.scene.add(actor.body);
actor.on('bodychange', async (amendPreviousState = false) => {
// console.log(`${actor.label} body change`);
if (this.bodyChangeHandlePromise === null) {
let resolve = null;
this.bodyChangeHandlePromise = new Promise(res => this.bodyChangeHandlePromiseResolver = res);
this.timer.trigger();
// this.bodyChangeHandlePromiseResolver.resolve = resolve;
}
await this.bodyChangeHandlePromise;
// console.log('resolved');
if (overwritePreviousState === true) {
// the bodychange event will trigger a state addition
// console.log('execute squash')
this.overwritePreviousState();
overwritePreviousState = false; // only do this the first time
}
this.timer.trigger();
});
this.timer.trigger();
}
/**
* List of sbi's
* @type {Array<SingleBlockInstance>}
*/
singleBlockInstances = [];
/**
* @param {Block} block
* @param {Vector3} position
* @param {Quaternion} quaternion
* @returns {SingleBlockInstance}
*/
addSingleBlockInstance(block, position, quaternion) {
if (this.pkgs.indexOf(block.tree) === -1) {
throw new Error('Block belongs to unknown tree');
}
const blockInstance = new BlockInstance(this._reporter, { block });
const SBI = new SingleBlockInstance(
this._reporter,
{
blockInstance: blockInstance,
loader: block.tree.loader,
position: position || new Vector3(),
quaternion: quaternion || new Quaternion()
}
);
this.addActor(SBI, this.singleBlockInstances);
return SBI;
}
/**
* List of configurators
* @type {Array<Configurator>}
*/
configurators;
/**
* @param {Object} settings
* @param {Package} settings.pkg
* @param {Boolean} [overwritePreviousState=false]
* @returns {Promise<Configurator>}
*/
async addConfigurator(settings, overwritePreviousState = false) {
if (this.pkgs.indexOf(settings.pkg) === -1) {
throw new Error('Unknown package');
}
// console.log('squash=', overwritePreviousState)
const configurator = new Configurator(this._reporter, settings);
this.addActor(configurator, this.configurators, overwritePreviousState);
return configurator;
}
// /**
// * List of transforms
// * @type {Object<string,Transform>}
// */
// transforms = {};
// /**
// * @param {typeof Transform} transformClass
// * @returns {Transform}
// */
// addTransformClass(transformClass) {
// this.transforms[transformClass.name] = new transformClass(
// this._reporter,
// {
// timer: this.timer,
// scene: this.scene,
// }
// );
// return this.transforms[transformClass.name];
// }
findConfigurator(configuration) {
return this.configurators.find(c => c.configuration === configuration);
}
/**
* List of views
* @type {Array<View>}
*/
views = [];
/**
* @param {View} [view]
*/
addView(view) {
if (!view) {
view = new DefaultView(
this._reporter,
{
scene: this.scene,
renderer: this.renderer,
rendererOrtho: this.rendererOrtho,
timer: this.timer,
findConfigurator: this.findConfigurator.bind(this),
addConfigurator: this.addConfigurator.bind(this),
findComponentById: this.findComponentById.bind(this)
}
);
}
this.views.push(view);
view.onSceneUpdate();
return view;
}
// the cursor was moved
/** @param {any} selectedState */
onStateCursorMoved(selectedState) {
// console.log('state cursor moved, selected state:', selectedState)
// console.log(this._stateRegister, this._cursor)
for (let actor of this.actors) {
if (selectedState[actor.slug] === undefined) {
if (actor.visible === true) {
console.log('hide', actor.label)
actor.hide();
}
}
}
for (let actor of this.actors) {
if (selectedState[actor.slug] !== undefined) {
if (actor.visible !== true) {
console.log('show', actor.label)
actor.show();
}
actor.setCursor(selectedState[actor.slug]);
}
}
}
/**
* @param {number} [steps =1]
* @returns {Promise<Object>}
*/
undo(steps = 1) {
// this.disableTransforms();
for (let view of this.views) {
view.removeAllMarkers();
}
return super.undo(steps);
}
/**
* @param {number} [steps =1]
* @returns {Promise<Object>}
*/
redo(steps = 1) {
// this.disableTransforms();
for (let view of this.views) {
view.removeAllMarkers();
}
return super.redo(steps);
}
removeConfigurator(configurator) {
const index = this.configurators.indexOf(configurator);
if (index === -1) {
throw new Error('Unknown configurator');
}
if (this.scene) {
this.scene.remove(configurator.body);
}
configurator.removeAllListeners();
configurator.clearBody();
configurator.resetState();
this.configurators.splice(index, 1);
}
// disableTransforms() {
// for (let transform of Object.values(this.transforms)) {
// transform.removeAll(true);
// }
// }
reset(removePkgs = false) {
this.resetState();
this.removeAllListeners();
if (this.configurators) {
for (let configurator of this.configurators) {
this.removeConfigurator(configurator);
}
}
this.configurators = [];
// pkgs weggooien is niet nodig
if (this.pkgs && removePkgs === true) {
for (let pkg of this.pkgs) {
pkg.removeAllListeners();
}
}
if (!this.pkgs || removePkgs === true) {
this.pkgs = [];
}
// clear scene
if (!this.scene) {
this.scene = new Scene();
}
// if ( this.scene ) {
// while (this.scene.children.length > 0) {
// this.scene.remove(this.scene.children[0]);
// }
// }
// this.scene = new Scene();
// remove any old update requests
for (let urId of Object.keys(this.timer.updateRequests)) {
this.timer.removeUpdateRequest(urId);
}
// create bounding box for the entire project
this.boundingBox = null;
}
/**
* @method
* @param {UUID|String} identifier Project id or slug
*/
async load(identifier) {
if ((!this.server) || (!this.server.connected)) {
throw new Error('Unable to load project, not connected to server.');
}
let projectData = null;
try {
const response = await this.server.request({
endpoint: 'project',
method: 'read',
data: {
identifier
}
});
projectData = JSON.parse(response.data.export);
this.report({ msg: `Project data load success`, level: 'notice' });
}
catch (err) {
console.error('Error while loading project:', err);
}
// console.log(projectData);
return this.buildProjectFromExport(projectData);
}
loadFromLocalStorage(key) {
let projectData = null;
if (localStorage) {
const projectDataStr = localStorage.getItem(key);
if (projectDataStr) {
projectData = JSON.parse(projectDataStr);
}
}
return projectData;
}
async buildProjectFromExport(projectData) {
this.reset();
for (let pkgToLoad of projectData.packages) {
const loadedPkg = this.pkgs.find(pkg => pkg.id === pkgToLoad.id);
if (loadedPkg) {
console.log(loadedPkg.label, 'already loaded')
}
else {
await this.addPackage(new URL(pkgToLoad.url));
}
}
const configurators = await Promise.all( projectData.configurations.map(async (configuratorData)=>{
const pkg = this.pkgs.find(pkg => pkg.id === configuratorData.configuration.pkg);
// console.log(pkg, this.pkgs, configuratorData)
const configuration = Configuration.createFromJSON(configuratorData.configuration, this._reporter, pkg);
const configCopy = configuration.copy()
const configurator = await this.addConfigurator({
pkg,
position: new Vector3(configuratorData.position.x, configuratorData.position.y, configuratorData.position.z),
quaternion: new Quaternion(configuratorData.quaternion.x, configuratorData.quaternion.y, configuratorData.quaternion.z, configuratorData.quaternion.w),
configuration: configCopy
});
// console.log('update configurator with config copy');
await configurator.update(configCopy)
// console.log('updated', configurator.cursor, configurator._stateRegister);
// console.log(configCopy);
// position: new Vector3( configuratorData.position.x, configuratorData.position.y, configuratorData.position.z ),
// quaternion: new Quaternion( configuratorData.quaternion._x,configuratorData.quaternion._y,configuratorData.quaternion._z,configuratorData.quaternion._w)
this.timer.trigger();
return configurator
}))
return configurators
}
async save({ copy = false, metadata = undefined } = {}) {
// wait for configs to build, otherwise assignables array can still be
// empty in export (and perhaps other issues too)
await Promise.all(this.configurators.map(c => c.configuration.build()));
const exp = {
meta: {
time: new Date().getTime()
},
packages: this.pkgs.map(pkg => ({
id: pkg.id,
url: pkg.loader.loadingBases[0].url.href
})),
configurations: this.configurators.map(
configurator =>
({
configuration: configurator.configuration.toJSON(),
position: configurator.state.position,
quaternion: configurator.state.quaternion
})
)
// singleBlockInstances: this.singleBlockInstances.map(SBI =>
// ({
// sbi: SBI.toJSON(),
// position: SBI.state.position,
// quaternion: SBI.state.quaternion
// })
// )
}
// console.log(exp)
const expStr = JSON.stringify(exp);
if (localStorage) {
// console.log('autosave');
localStorage.setItem('autosave', expStr);
}
let returnObject = { export: exp };
if ((!this.server) || (!this.server.connected)) {
throw new Error('Unable to save project to server, not connected.');
}
else {
try {
const saveResponse = await this.server.request({
endpoint: 'project',
method: copy === true ? 'create' : 'update',
data: {
id: this.id,
name: '',
description: '',
export: expStr,
metadata: metadata ? JSON.stringify( metadata ) : undefined
}
});
// console.log('saveResponse', saveResponse)
if (saveResponse.data) {
this.report({ msg: 'Project saved', level: 'notice' });
this.slug = returnObject.slug = saveResponse.data.slug;
this.price = returnObject.price = saveResponse.data.price;
}
else {
this.report({ msg: 'Project could not be saved: ' + saveResponse.errors[0]?.description, level: 'error' });
this.price = undefined;
}
}
catch (err) {
console.error('Error while saving project:', err);
}
}
return returnObject;
}
/**
* Search the loaded packages and current confugurations for a component
* with the supplied id
* @param {UUID} id
* @returns {Component}
*/
findComponentById(id) {
let component = undefined;
for (let pkg of this.pkgs) {
component = pkg.findComponentById(id);
if (component) {
break;
}
}
if (component === undefined) {
for (let configurator of this.configurators) {
component = configurator.configuration.findComponentById(id);
if (component) {
break;
}
}
}
return component;
}
/**
* Sets a theme accross all configurators for the relevant package
* and squashes their state updates into one
* @param {Theme} theme
*/
setTheme(theme) {
for (let i = 0, l = this.configurators.length; i < l; i += 1) {
this.configurators[i].setTheme(theme);
if (i > 0) {
this.overwritePreviousState();
}
}
}
}
export { Project as Project };