aframe-connecting-line
Version:
Uses the A-Frame "line" component to draw a line between points on two entities.
218 lines (179 loc) • 7.11 kB
JavaScript
/* global AFRAME THREE */
(() => {
const _startVector = new THREE.Vector3()
const _endVector = new THREE.Vector3()
const _lineVector = new THREE.Vector3()
const _posVector = new THREE.Vector3()
const _deltaVector = new THREE.Vector3()
const _lineDirectionVector = new THREE.Vector3()
const _up = new THREE.Vector3(0, 1, 0)
AFRAME.registerComponent('connecting-line', {
schema: {
start: {type: 'selector'},
startOffset: {type: 'vec3', default: {x: 0, y: 0, z: 0}},
end: {type: 'selector'},
endOffset: {type: 'vec3', default: {x: 0, y: 0, z: 0}},
color: {type: 'color', default: '#74BEC1'},
opacity: {type: 'number', default: 1},
visible: {default: true},
lengthAdjustment: {default: "none", oneOf: ["none", "scale", "extend", "absolute"]},
lengthAdjustmentValue: {type: 'number'},
width: {type: 'number'},
segments: {type: 'number', default: 4},
shader: {type: 'string', default: 'flat'},
updateEvent: {type: 'string', default: ''}
},
multiple: true,
init() {
this.cylinder = null
this.listenerData = {
event: '',
start: null,
end: null
}
this.updateLine = this.updateLine.bind(this)
},
update() {
this.el.setAttribute(`line__${this.attrName}`,
{
color: this.data.color,
opacity: this.data.opacity,
visible: this.data.visible
})
if (this.data.width > 0) {
if (!this.cylinder) {
this.cylinder = document.createElement('a-cylinder')
this.el.appendChild(this.cylinder)
}
this.cylinder.setAttribute('radius', this.data.width / 2)
this.cylinder.setAttribute('segments-radial', this.data.segments)
this.cylinder.setAttribute('segments-height', 1)
this.cylinder.setAttribute('material',
{
shader: this.data.shader,
color: this.data.color,
opacity: this.data.opacity,
visible: this.data.visible
})
}
else if (this.cylinder) {
this.el.removeChild(this.cylinder)
this.cylinder = null
}
this.updateEventListeners()
// initial update should be deferred until scene load has completed.
setTimeout(this.updateLine)
},
updateEventListeners() {
if (!this.data.updateEvent ||
(this.listenerData.event &&
this.data.updateEvent !== this.listenerData.event)) {
this.removeEventListeners()
}
if (this.data.updateEvent) {
this.addAndTrackEventListeners()
}
},
addAndTrackEventListeners() {
const { data, listenerData } = this
data.start.addEventListener(data.updateEvent, this.updateLine)
data.end.addEventListener(data.updateEvent, this.updateLine)
this.el.addEventListener(data.updateEvent, this.updateLine)
listenerData.event = data.updateEvent
listenerData.start = data.start
listenerData.end = data.end
},
removeEventListeners() {
const { listenerData } = this
if (listenerData.start) {
listenerData.start.removeEventListener(listenerData.event, this.updateLine)
}
if (listenerData.end) {
listenerData.start.removeEventListener(listenerData.event, this.updateLine)
}
this.el.removeEventListener(listenerData.event, this.updateLine)
},
remove() {
this.el.removeAttribute(`line__${this.attrName}`)
if(this.cylinder) {
this.el.removeChild(this.cylinder)
this.cylinder = null
}
this.removeEventListeners()
},
// Position is updated on a tick() to accommodate movement of entities, which may
// not be in the object hierarchy above the entity that the line is configured on.
adjustLength(start, end) {
const { lengthAdjustment } = this.data
const delta = _deltaVector
switch (lengthAdjustment) {
case "none":
return
case "scale": {
const lengthFactor = this.data.lengthAdjustmentValue
if (!lengthFactor) return
delta.subVectors(end, start)
const scaleExtension = 1 + ((lengthFactor - 1) / 2)
delta.multiplyScalar(scaleExtension)
break
}
case "extend": {
const extension = this.data.lengthAdjustmentValue
if (!extension) return
delta.subVectors(end, start)
delta.normalize()
delta.multiplyScalar(-extension)
break
}
case "absolute": {
const targetLength = this.data.lengthAdjustmentValue
if (!targetLength) return
delta.subVectors(end, start)
const currentLength = delta.length()
if (currentLength === 0) return
const absExtension = ((targetLength / currentLength) - 1) / 2
delta.multiplyScalar(-absExtension)
break
}
default:
console.error("Unexpected value for lengthAdjustment: ", lengthAdjustment)
return
}
start.addVectors(start, delta)
end.subVectors(end, delta)
},
tick() {
if (this.data.updateEvent) return
this.updateLine()
},
updateLine() {
// Because calls to this function can be deferred, handle race condition where component has
// already been removed.
if (!this.data) return
const start = _startVector
const end = _endVector
// transform start & end vectors to local space.
start.copy(this.data.startOffset)
this.data.start.object3D.updateMatrixWorld()
this.data.start.object3D.localToWorld(start)
this.el.object3D.worldToLocal(start)
end.copy(this.data.endOffset)
this.data.end.object3D.updateMatrixWorld()
this.data.end.object3D.localToWorld(end)
this.el.object3D.worldToLocal(end)
this.adjustLength(start, end)
this.el.setAttribute(`line__${this.attrName}`,
`start: ${start.x} ${start.y} ${start.z};
end: ${end.x} ${end.y} ${end.z}`)
if (this.cylinder) {
_lineVector.subVectors(end, start)
_posVector.addVectors(start, end).multiplyScalar(0.5)
_lineDirectionVector.copy(_lineVector).normalize()
const object = this.cylinder.object3D
object.position.copy(_posVector)
object.quaternion.setFromUnitVectors(_up, _lineDirectionVector)
this.cylinder.setAttribute('height', _lineVector.length())
}
}
})
})()