awv3
Version:
⚡ AWV3 embedded CAD
532 lines (475 loc) • 20.9 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 { 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)
}