UNPKG

awv3

Version:
532 lines (475 loc) 20.9 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 { 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, undo: { count: 0, current: 0 }, messages: [], plugins: [], tree: { '1': { id: 1, name: '', class: '', parent: null, dimensions: [], coordinateSystem: [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], }, 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, ) undo(factory = this.session.options.materials.factory) { return this.store.dispatch(this.actions.request(this.id, undefined, factory, 'undo')) } redo(factory = this.session.options.materials.factory) { return this.store.dispatch(this.actions.request(this.id, undefined, factory, 'redo')) } 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 } removePrimitives() { Object.values(this.primitives).forEach(item => item.destroy()) 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: () => { console.log('assembly is gone', container.name) Object.values(instances).forEach(item => item.destroy()) instances = {} container.destroy() parent === this.pool && 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.meshShader({ color: new THREE.Color( `rgb(${part.color ? part.color.join(',') : '190, 190, 190'})`, ), ...this.session.options.meshShaderOptions, }), ) 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: () => { console.log('part is gone', container.name) Object.values(clones).forEach(item => item.destroy()) clones = {} container.destroy() parent === this.pool && this.removePrimitives() }, fireOnStart: true, unsubscribeOnUndefined: true, }, ) return container } _instanciateFeature(id, props) { let { parent, feature, persistent, pluginPrototype } = this.featurePlugins[id] // If activeSelection is on we must reset it !persistent && 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) }