awv3
Version:
⚡ AWV3 embedded CAD
506 lines (450 loc) • 20.1 kB
JavaScript
import * as THREE from 'three';
import memoize from 'lodash/memoize';
import cloneDeep from 'lodash/cloneDeep';
import throttle from 'lodash/throttle';
import Lifecycle from './lifecycle.js';
import { arrayDiff } from './helpers';
import { flush } from './store/';
import { actions } from './store/connections';
import { actions as globalActions } from './store/globals';
import { actions as pluginActions, base as pluginBase } from './store/plugins';
import { halt } from '../core/error';
import Base from '../communication/base';
/**
* @class Connection manages a local or remote connection to a running ClassCAD instances.
ClassCAD will then drive the tree-state and cause visual actions, models, meshes, movement, etc.
In theory a ClassCAD instance is not needed, results could be fed statically using parse or stream.
*/
export default class Connection extends Lifecycle(Base, false) {
constructor(session = halt('connection must be initialized with a session'), props) {
super(session, actions, state => state.connections[this.id], {
name: 'Part',
...props,
connected: false,
activeFeature: undefined,
defaultFeatureVisibility: false,
messages: [],
plugins: [],
tree: { '1': { id: 1, name: '', class: '', parent: null }, root: 1 },
managed: false
});
this.pool = new session.options.pool({
session,
name: `connection.pool.${this.name}`,
...session.options.materials
});
this.primitives = {};
this.waiting = {};
this.featurePlugins = {};
this.sequence = Promise.resolve();
// Observe tree changes to clear cache
this.observe(state => state.tree, () => this._get.cache.clear());
// Observe root
this.observe(state => state.tree.root, root => this._observeLink(root));
// Observe active feature
this.observe(state => state.activeFeature, (feature, oldFeature) => {
if (oldFeature) {
let data = this.featurePlugins[oldFeature];
this.store.dispatch(pluginActions.update(data.plugin.id, { enabled: false, collapsed: true }));
if (!data.pluginPrototype.persistent) {
this._destroyFeatures(oldFeature);
}
}
if (feature) {
let data = this.featurePlugins[feature];
if (data.pluginPrototype.persistent) {
this.store.dispatch(pluginActions.update(data.plugin.id, { enabled: true, collapsed: false }));
this.activeFeaturePlugin = data.plugin;
} else {
this.activeFeaturePlugin = this._instanciateFeature(feature);
this.store.dispatch(
pluginActions.update(this.activeFeaturePlugin.id, { enabled: true, collapsed: false })
);
}
}
});
// Connect to server
if (this.session.options.url && this.session.options.protocol) {
this.connect(this.session.options.url);
}
}
async defer(func) {
return new Promise(async resolve => {
await this.sequence;
requestIdleCallback(async () => resolve(await func()));
});
}
updateView = throttle(
({ focus = true, zoom = true, rotate = false, layers = false } = this.session.options.updateView) => {
this.pool.view.updateBounds();
focus && this.pool.view.controls.focus();
zoom && this.pool.view.controls.zoom();
rotate && this.pool.view.controls.rotate(...rotate);
this.pool.update();
layers && this.session.showLayer(this.session.layerNames);
},
this.session.options.throttle
);
request(command, factory = this.session.options.materials.factory) {
return this.store.dispatch(this.actions.request(this.id, command, factory));
}
stream(url, factory = this.session.options.materials.factory) {
return this.store.dispatch(this.actions.stream(this.id, url, factory));
}
parse(blob, factory = this.session.options.materials.factory) {
return this.store.dispatch(this.actions.parse(this.id, blob, factory));
}
connect(url) {
return this.store.dispatch(this.actions.connect(this.id, url, this.session.options.protocol));
}
setTree(payload) {
this.store.dispatch(actions.destroyTree(this.id));
this.store.dispatch(actions.setTree(this.id, payload));
}
patchTree(payload) {
return this.store.dispatch(this.actions.patchTree(this.id, payload));
}
resolveTree(args) {
if (!args) return;
args = Array.isArray(args) ? args : [args];
let result = args.map(key => this._get(key, this.getState().tree));
return result;
}
async removePrimitives() {
await Promise.all(Object.values(this.primitives).map(item => item.destroyAsync()));
this.primitives = {};
this.updateView();
}
_observeLink(id, parent, instance) {
let tree = this.getState().tree;
let object = tree[id];
switch (object.class) {
case 'CC_Assembly':
//console.log("observing", object.name)
return this._observeAssembly(id, parent, instance);
case 'CC_Part':
case 'CC_SheetPart':
let container = this._observePart(id, parent, instance);
this.defer(() => this._observeFeatures(id, container));
return container;
}
}
_observeAssembly(root, parent = this.pool, instance) {
let instances = {};
let container = new this.session.options.objectPrototype({ session: this.session });
container.name = 'Assembly';
container.userData = { id: instance };
parent.add(container);
this.observe(
// selector
state => (state.tree[root] || {}).instances,
// onChange
async (...args) => {
let tree = this.tree;
await arrayDiff(
...args,
newItems => newItems.forEach(item => {
let instance = tree[item];
let link = undefined;
let object = undefined;
// Observe changes to it
this.observe(
state => state.tree[item],
instance => {
// Links should be reactive as well
if (instance.link !== link) {
if (object) {
object.destroy();
delete instances[instance.id];
}
object = instances[item] = this._observeLink(instance.link, container, item);
object.matrixAutoUpdate = false;
link = instance.link;
}
let csys = instance.coordinateSystem;
object.userData.name = instance.name;
object.name = instance.name;
object.visible = typeof instance.visible !== 'undefined' ? instance.visible : true;
object.matrix = new THREE.Matrix4()
.makeBasis(
new THREE.Vector3(...csys[1]),
new THREE.Vector3(...csys[2]),
new THREE.Vector3(...csys[3])
)
.setPosition(new THREE.Vector3(...csys[0]));
this.updateView();
//console.log(` moving instance ${instance.name} to ${JSON.stringify(csys[0])}`);
},
{ fireOnStart: true, unsubscribeOnUndefined: true }
);
}),
deletedItems => deletedItems.forEach(item => {
let instance = instances[item];
//console.log(` removing instance ${instance.name}`);
delete instances[item];
return instance.destroy();
})
);
this.updateView();
},
// options
{
onRemove: async () => {
console.log('assembly is gone', container.name);
await Promise.all(Object.values(instances).map(item => item.destroyAsync()));
instances = {};
await container.destroyAsync();
parent === this.pool && (await this.removePrimitives());
},
fireOnStart: true,
unsubscribeOnUndefined: true
}
);
return container;
}
_observePart(root, parent = this.pool, instance) {
let clones = {};
let container = new this.session.options.objectPrototype({ session: this.session });
container.name = 'Part';
container.temporary = new THREE.Group();
container.temporary.name = 'Part.temporaray';
container.temporary.updateParentMaterials = false;
container.userData = { id: instance };
container.add(container.temporary);
parent.add(container);
this.observe(
// selector
state => (state.tree[root] || {}).solids,
// onChange
async (...args) => {
let part = this.tree[root];
await arrayDiff(
...args,
newItems => {
newItems.map(item => {
let primitive = this.primitives[item];
if (!primitive) {
// primitive is not yet known
// Create bounds representation
let mesh;
if (part.min && part.max) {
let min = part.min;
let max = part.max;
mesh = new THREE.Mesh(
new THREE.BoxBufferGeometry(max[0] - min[0], max[1] - min[1], max[2] - min[2]),
new this.session.options.materials.meshes.shader({
color: new THREE.Color(
`rgb(${part.color ? part.color.join(',') : '190, 190, 190'})`
),
...this.session.options.materials.meshes.options
})
);
if (this.session.options.materials.shadows) {
mesh.castShadow = true;
mesh.receiveShadow = true;
}
mesh.position.set(
(max[0] + min[0]) / 2,
(max[1] + min[1]) / 2,
(max[2] + min[2]) / 2
);
container.reset(mesh).add(mesh);
}
// If none exists, create empty waiting list entry from item
if (!this.waiting[item]) this.waiting[item] = [];
// Register item on the waiting-list
this.waiting[item].push(primitive => {
// Delete bounds
mesh && mesh.destroyAsync();
// Create real clone, finally
createClone({ root, parent, clones, container, primitive, item });
});
} else
createClone({ root, parent, clones, container, primitive, item });
});
this.updateView();
},
deletedItems => deletedItems.map(item => {
let clone = clones[item];
// Remove clone from primitives
let primitive = this.primitives[item];
if (primitive) primitive.references.splice(primitive.references.indexOf(clone), 1);
delete clones[item];
return clone.destroyAsync();
})
);
},
// options
{
onRemove: async () => {
console.log('part is gone', container.name);
await Promise.all(Object.values(clones).map(item => item.destroyAsync()));
clones = {};
await container.destroyAsync();
parent === this.pool && (await this.removePrimitives());
},
fireOnStart: true,
unsubscribeOnUndefined: true
}
);
return container;
}
_instanciateFeature(id, props) {
let { parent, feature, pluginPrototype } = this.featurePlugins[id];
// If activeSelection is on we must reset it
this.session.globals.activeSelection && this.store.dispatch(globalActions.setActiveSelection());
// Each plugin gets a distinct pool to draw into
let pool = new THREE.Group();
pool.name = 'Instance';
pool.updateParentMaterials = false;
pool.measurable = pluginPrototype.measurable;
pool.name = feature.name;
parent.add(pool);
// Intanciate and link new plugin
const plugin = new pluginPrototype(this.session, {
name: feature.name,
feature,
pool,
connection: this,
...props
});
// Link plugins
this.featurePlugins[id].plugin = plugin;
this.store.dispatch(actions.linkPlugins(this.id, plugin.id));
return plugin;
}
_mapFeatures(parent, ...features) {
for (let id of features) {
let feature = this.tree[id];
let type = feature.class;
let pluginPrototype = this.session.featurePlugins[type];
if (!pluginPrototype && this.session.defaultFeaturePlugin) pluginPrototype = this.session.defaultFeaturePlugin;
if (pluginPrototype) {
// Map feature plugin
this.featurePlugins[id] = {
feature,
parent: parent.temporary,
pluginPrototype,
persistent: pluginPrototype.persistent
};
if (pluginPrototype.persistent) {
// Return persistent plugins
this._instanciateFeature(id);
}
}
}
}
_unmapFeatures(...features) {
this._destroyFeatures(features);
features.forEach(id => delete this.featurePlugins[id]);
}
_destroyFeatures(...features) {
let plugins = features
.map(id => this.featurePlugins[id])
.filter(data => data && data.plugin)
.map(data => data.plugin.id);
if (plugins.length > 0) {
this.store.dispatch(actions.unlinkPlugins(this.id, plugins));
}
features.forEach(id => {
// Find real plugin class
let { plugin, parent, persistent } = this.featurePlugins[id] || {};
if (plugin) {
if (!persistent) {
delete this.featurePlugins[id].plugin;
}
parent.remove(plugin.pool);
this.defer(() => this.store.dispatch(pluginActions.unregister(plugin.id)));
}
});
}
_observeFeatures(root, parent = this.pool) {
//console.log("observeFeatures...")
return this.observe(
// selector
state => (state.tree[root] || {}).features,
// onChange
(...args) => {
arrayDiff(
...args,
newItems => this._mapFeatures(parent, ...newItems),
deletedItems => this._unmapFeatures(...deletedItems)
);
},
// options
{ fireOnStart: true, unsubscribeOnUndefined: true }
);
}
_resolveArray(array, state) {
for (let member of array) {
if (member.type === 'id') {
array[array.indexOf(member)] = this._get(member.value, state);
} else if (member.type === 'array') {
this._resolveArray(member, state);
}
}
}
_get = memoize((key, state = this.getState().tree) => {
if (this.store) {
let obj = state[key];
if (obj) {
obj = cloneDeep(state[key]);
if (obj.children) obj.children = obj.children.map(item => this._get(item, state));
if (obj.members) {
for (let key in obj.members) {
let member = obj.members[key];
if (key !== 'mateSystem' && member.type === 'id') {
obj.members[key] = this._get(member.value, state);
} else if (member.type === 'array') {
this._resolveArray(member.members, state);
}
}
}
}
return obj;
}
});
__onDestroyed() {
Object.values(this.primitives).forEach(item => item.destroy());
this.primitives = {};
Object.values(this.session.plugins)
.forEach(
plugin => plugin.connection === this.id && this.store.dispatch(pluginActions.unregister(plugin.id))
);
if (this.session.globals.activeConnection === this.id) {
this.session.dispatch(globalActions.setActiveConnection());
}
this.socket && this.socket.disconnect();
}
}
function createClone({ root, parent, clones, container, primitive, item }) {
let clone = clones[item] = primitive.clone();
primitive.references.push(clone);
// Calculate new deep ID
let array = [root];
parent = container;
while (parent && parent.userData && parent.userData.id) {
array.push(parent.userData.id);
parent = parent.parent;
}
array = array.reverse();
// deep clone materials;
for (let child of clone.children.keys()) {
let c = clone.children[child];
let p = primitive.children[child];
if (c.material) {
let isMultiMaterial = Array.isArray(c.material);
if (isMultiMaterial) {
for (let i = 0,l = c.material.length; i < l; i++) {
let pMaterial = p.material[i];
let cMaterial = c.material[i] = pMaterial.clone(true);
cMaterial.meta = { ...pMaterial.meta, originalId: pMaterial.meta.id, id: [...array, pMaterial.meta.id].join(',') };
}
} else {
c.material = p.material.clone(true);
c.material.meta = { ...p.material.meta };
}
}
}
return container.reset(clone).add(clone);
}