UNPKG

awv3

Version:
506 lines (450 loc) 20.1 kB
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); }