src/three/Additions.js
import Attributes from '../style/Attributes.js'
import NodeStyles from '../style/NodeStyles.js'
import { SelectorFragmentList } from '../style/Selector.js'
/**
importing this extends THREE.Object3D with many methods and attributes useful for creating and manipulating the SOM
*/
const _workingVector3_1 = new THREE.Vector3()
const _workingVector3_2 = new THREE.Vector3()
// Used by KSS tag selectors
THREE.Object3D.prototype.isNode = true
THREE.Scene.prototype.isScene = true
/**
Expands or contracts along the XR plane by:
- adding top to this.max.y
- adding right to this.max.x
- subbtracting bottom from this.min.y
- subbtracting left from this.min.x
*/
THREE.Box3.prototype.changeXYPlane = function(top, right, bottom, left) {
this.max.y += top
this.max.x += right
this.min.y -= bottom
this.min.x -= left
}
THREE.Box3.prototype.makeZero = function() {
this.min.set(0, 0, 0)
this.max.set(0, 0, 0)
return this
}
THREE.Box3.prototype.centerAtOrigin = function() {
this.getCenter(_workingVector3_1)
_workingVector3_1.negate()
this.translate(_workingVector3_1)
}
THREE.Box3.prototype.scale = function(vec3) {
this.min.x *= vec3.x
this.min.y *= vec3.y
this.min.z *= vec3.z
this.max.x *= vec3.x
this.max.y *= vec3.y
this.max.z *= vec3.z
}
Object.defineProperty(THREE.Object3D.prototype, 'styles', {
/**
Object3D.styles holds the KSS and layout information for an Object3D
@type {NodeStyles}
*/
get: function() {
if (this._styles === undefined) {
this._styles = new NodeStyles(this)
}
return this._styles
}
})
/**
Set the styles.hierarchyIsDirty when adding or removing a child
*/
const _oldAdd = THREE.Object3D.prototype.add
THREE.Object3D.prototype.add = function(...objects) {
let shouldSetDirty = false
for (let i = 0; i < objects.length; i++) {
_oldAdd.call(this, objects[i])
if (objects[i].shadowSOM !== true) {
shouldSetDirty = true
}
}
if (shouldSetDirty) {
this.styles.setAncestorsHierarchyDirty()
}
return this
}
const _oldRemove = THREE.Object3D.prototype.remove
THREE.Object3D.prototype.remove = function(...objects) {
for (let i = 0; i < objects.length; i++) {
_oldRemove.call(this, objects[i])
}
this.styles.setAncestorsHierarchyDirty()
return this
}
/**
Override the Object3D.visible property in order to update styles when it changes
*/
Object.defineProperty(THREE.Object3D.prototype, 'visible', {
get: function() {
return this._visible !== false
},
set: function(val) {
if (this._visible === val) return
this._visible = val
if (this.shadowSOM !== true) {
this.styles.setAncestorsHierarchyDirty()
this.styles.setSubgraphStylesDirty()
}
}
})
THREE.Object3D.prototype.toggleClass = function(on, ...classNames) {
if (on) {
return this.addClass(...classNames)
} else {
return this.removeClass(...classNames)
}
}
/**
Helper functions to handling classes used by the Stylist
*/
THREE.Object3D.prototype.addClass = function(...classNames) {
if (this.userData.classes === undefined) {
this.userData.classes = [...classNames]
this.styles.setSubgraphStylesDirty()
return this
}
let shouldSetDirty = false
for (let i = 0; i < classNames.length; i++) {
if (this.userData.classes.includes(classNames[i])) continue
this.userData.classes.push(classNames[i])
shouldSetDirty = true
}
if (shouldSetDirty) this.styles.setSubgraphStylesDirty()
return this
}
THREE.Object3D.prototype.removeClass = function(...classNames) {
if (this.userData.classes === undefined || this.userData.classes.length === 0) return this
let shouldSetDirty = false
for (let i = 0; i < classNames.length; i++) {
const index = this.userData.classes.indexOf(classNames[i])
if (index === -1) continue
this.userData.classes.splice(index, 1)
shouldSetDirty = true
}
if (shouldSetDirty) this.styles.setSubgraphStylesDirty()
return this
}
THREE.Object3D.prototype.hasClass = function(className) {
if (this.userData.classes === undefined) return false
return this.userData.classes.includes(className)
}
THREE.Object3D.prototype.getClasses = function() {
if (this.userData.classes === undefined) return []
return this.userData.classes
}
/**
Calls showEdges or hideEdges to toggle the debugging edge boxes
*/
THREE.Object3D.prototype.toggleEdges = function(includingChildren = false) {
if (this._marginBox) {
this.hideEdges(includingChildren)
} else {
this.showEdges(includingChildren)
}
return this
}
/**
If the Object3D has a geometry then show a debugging box around it
*/
THREE.Object3D.prototype.showEdges = function(includingChildren = false) {
if (this._marginBox === undefined) {
if (_edgeBoxMaterial === null) {
_edgeBoxMaterial = new THREE.LineBasicMaterial({
color: 0x660000,
linewidth: 200
})
}
if (_edgeBoxGeometry === null) {
_edgeBoxGeometry = THREE.MakeCubeGeometry(1)
}
this._marginBox = new THREE.LineSegments(_edgeBoxGeometry, _edgeBoxMaterial)
this._marginBox.addClass('margin-box')
this._marginBox.styles.assignedStyles.set('position', 'absolute')
this._marginBox.shadowSOM = true
this.add(this._marginBox)
}
this.styles.marginBounds.getSize(_workingVector3_1)
// Scales can't be zero
_workingVector3_1.x = Math.max(0.0001, _workingVector3_1.x)
_workingVector3_1.y = Math.max(0.0001, _workingVector3_1.y)
_workingVector3_1.z = Math.max(0.0001, _workingVector3_1.z)
this._marginBox.scale.copy(_workingVector3_1)
this.styles.marginBounds.getCenter(_workingVector3_2)
this._marginBox.position.copy(_workingVector3_2)
if (includingChildren) {
for (const child of this.children) {
if (child.visible === false) continue
if (child.shadowSOM === true) continue
if (child.hasClass('margin-box')) continue
child.showEdges(true)
}
}
return this
}
/**
Remove boxes shown by `Object3D.showEdges`
*/
THREE.Object3D.prototype.hideEdges = function(includingChildren = false) {
if (this._marginBox !== undefined) {
this.remove(this._marginBox)
this._marginBox = undefined
}
if (includingChildren) {
for (const child of this.children) {
if (child.hasClass('margin-box')) continue
if (child.shadowSOM === true) continue
child.hideEdges(true)
}
}
return this
}
THREE.Object3D.prototype.findRoot = function(node = this) {
if (node.parent === null) return node
return node.findRoot(node.parent)
}
/**
A handy function for depth first traversal of all children and this node
@param {function} func a function of the signature function(Object3D)
*/
THREE.Object3D.prototype.traverseDepthFirst = function(func) {
_traverseDepthFirst(this, func)
}
const _traverseDepthFirst = function(node, func) {
for (let i = 0; i < node.children.length; i++) {
_traverseDepthFirst(node.children[i], func)
}
func(node)
}
/**
@param {string} selector - like 'node[name=ModeSwitcherComponent] .button-component > text'
@param {boolean} atMostOne - true if only one result is desired
@return {Object3D[]} nodes that match the selector
*/
THREE.Object3D.prototype.getObjectsBySelector = function(selector, atMostOne = false) {
const selectorFragmentList = SelectorFragmentList.Parse(selector)
const results = []
this.traverse(node => {
if (node === this) return
if (node.shadowSOM !== true && selectorFragmentList.matches(node)) {
results.push(node)
if (atMostOne) return results
}
})
return results
}
/**
@param {string} selector - like 'node[name=ModeSwitcherComponent] .button-component > text'
@return {Object3D?} the first node to match the selector or null if none were found
*/
THREE.Object3D.prototype.querySelector = function(selector) {
const results = this.getObjectsBySelector(selector, true)
if (results.length > 0) return results[0]
return null
}
/**
@param {Object3D} node
@return {Object3D[]} returns an array of the ancesters of `node`, starting at the root and ending with the `node`
*/
THREE.Object3D.prototype.getAncestry = function(node = this) {
const lineage = []
let workingNode = node
while (workingNode) {
lineage.push(workingNode)
workingNode = workingNode.parent
}
lineage.reverse()
return lineage
}
/**
Logs the ancestry of `node` starting with the root and ending with the `node`
@param {Object3D} [node=this]
@param {boolean} [showVars=false]
@param {boolean} [localsOnly=false]
*/
THREE.Object3D.prototype.logAncestry = function(node = this, showVars = false, localsOnly = false) {
node.getAncestry().forEach(obj => {
obj
._getStyleTreeLines(undefined, undefined, undefined, showVars, localsOnly, false)
.forEach(line => console.log(line))
})
}
/**
logs to the console the computed styles for a node and its descendents
@param {THREE.Object3D} node
@param {int} [tabDepth=0]
@param {bool} [showVars=false] if true, log the CSS variables of the form `--name`
@param {bool} [localsOnly=false] if true, show the local instead of the computed styles
*/
THREE.Object3D.prototype.logStyles = function(node = this, tabDepth = 0, showVars = false, localsOnly = false) {
this._getStyleTreeLines(node, [], tabDepth, showVars, localsOnly).forEach(line => console.log(line))
}
/**
@param {THREE.Object3D} node
@param {int} [tabDepth=0]
@param {bool} [showVars=false] if true, log the CSS variables of the form `--name`
@param {bool} [localsOnly=false] if true, show the local instead of the computed styles
@return {string} a string describing the computed styles for a node and its descendents
*/
THREE.Object3D.prototype.getStyleTree = function(node = this, tabDepth = 0, showVars = false, localsOnly = false) {
return this._getStyleTreeLines(node, [], tabDepth, showVars, localsOnly).join('\n')
}
/**
@param {THREE.Object3D} node
@param {string[]} [results=[]] an accumulator array
@param {int} [tabDepth=0]
@param {boolean} [showVars=false] if true, log the CSS variables of the form `--name`
@param {boolean} [localsOnly=false] if true, show the local instead of the computed styles
@param {boolean} [traverseChildren=true]
@return {string} a string describing the computed styles for a node and its descendents
*/
THREE.Object3D.prototype._getStyleTreeLines = function(
node = this,
results = [],
tabDepth = 0,
showVars = false,
localsOnly = false,
traverseChildren = true
) {
const tabs = _generateTabs(tabDepth)
results.push(
tabs +
'> ' +
(node.name || 'unnamed') +
(node.type ? `[type=${node.type}] ` : ': ') +
node
.getClasses()
.map(clazz => `.${clazz}`)
.join('') +
(node.hierarchyIsDirty ? '\tdirty' : '')
)
if (localsOnly) {
for (const styleInfo of node.styles.localStyles) {
if (showVars === false && styleInfo.property.startsWith('--')) continue
reults.push(tabs + '\t' + styleInfo.toString())
}
} else {
for (const styleInfo of node.styles.computedStyles) {
if (showVars === false && styleInfo.property.startsWith('--')) continue
results.push(tabs + '\t' + styleInfo.toString())
}
}
if (traverseChildren === false) {
return results
}
for (const child of node.children) {
if (child.shadowSOM === true) continue
this._getStyleTreeLines(child, results, tabDepth + 1, showVars, localsOnly)
}
return results
}
const _generateTabs = function(depth) {
if (depth === 0) return ''
const result = []
result[depth - 1] = null
return result.fill('\t').join('')
}
/**
Object3D.attributes is a helper API for accessing attributes on the node or in node.userData
*/
Object.defineProperty(THREE.Object3D.prototype, 'attributes', {
/**
@type {Attributes}
*/
get: function() {
if (typeof this._attributes === 'undefined') this._attributes = new Attributes(this)
return this._attributes
}
})
/**
Logs to the console info about this node
*/
THREE.Object3D.prototype.prettyPrint = function(depth = 0) {
let tabs = ''
for (let i = 0; i < depth; i++) {
tabs += ' '
}
console.log(tabs, (this.name || 'unnamed') + ':')
console.log(tabs + '\tscale:', ...this.scale.toArray())
console.log(tabs + '\tposition:', ...this.position.toArray())
console.log(tabs + '\tquaternion:', ...this.quaternion.toArray())
for (let i = 0; i < this.children.length; i++) {
if (this.children[i].shadowSOM === true) continue
this.children[i].prettyPrint(depth + 1)
}
}
/**
Looks in this node and up the ancestors until it finds a {Component} attribute
@return {Component|null}
*/
THREE.Object3D.prototype.getComponent = function() {
let obj = this
while (true) {
if (obj.component) return obj.component
if (!obj.parent) return null
obj = obj.parent
}
}
/** A convenience function to allow chaining like `let group = som.group().appendTo(scene)` */
THREE.Object3D.prototype.appendTo = function(parent) {
parent.add(this)
return this
}
/** A convenience function to allow appending dictionaries of attributes, arrays of subchildren, or children */
THREE.Object3D.prototype.append = function(child = null) {
if (child === null) {
return
}
if (typeof child === 'object' && typeof child.matrixWorld === 'undefined') {
// If it's an object but not an Object3D, consider it a dictionary of attributes
for (const key in child) {
if (child.hasOwnProperty(key) == false) continue
this[key] = child[key]
}
} else {
this.add(child)
}
return this
}
THREE.MakeCubeGeometry = function(size) {
const h = size * 0.5
const geometry = new THREE.BufferGeometry()
const position = []
position.push(
-h,
-h,
-h,
-h,
h,
-h,
-h,
h,
-h,
h,
h,
-h,
h,
h,
-h,
h,
-h,
-h,
h,
-h,
-h,
-h,
-h,
-h,
-h,
-h,
h,
-h,
h,
h,
-h,
h,
h,
h,
h,
h,
h,
h,
h,
h,
-h,
h,
h,
-h,
h,
-h,
-h,
h,
-h,
-h,
-h,
-h,
-h,
h,
-h,
h,
-h,
-h,
h,
h,
h,
h,
-h,
h,
h,
h,
h,
-h,
-h,
h,
-h,
h
)
geometry.addAttribute('position', new THREE.Float32BufferAttribute(position, 3))
return geometry
}
let _edgeBoxMaterial = null
let _edgeBoxGeometry = null
export {}