260 lines
9.5 KiB
JavaScript
260 lines
9.5 KiB
JavaScript
import * as THREE from 'three'
|
||
// import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.module.js'
|
||
// import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.module.js'
|
||
// import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.module.js'
|
||
// import { ShaderPass } from 'three/examples/jsm/shaders/GammaCorrectionShader.module.js'
|
||
// import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.module.js'
|
||
|
||
|
||
if(!app.helpers) app.helpers = {}
|
||
/**
|
||
* Mixing add-in methods to your view instance.
|
||
* All of this should not be a helper, but inherited this from WindozDomContent, but not my framework anymore.
|
||
*/
|
||
app.helpers.helpers3D = {
|
||
|
||
agentFromJSON(id, desc){
|
||
let obj, wrapper
|
||
if(desc.type === 'Mesh') {
|
||
const geom = new THREE[desc.geometry.type](...(desc.geometry.args || []))
|
||
const matType = desc.material.type
|
||
const matProps = { ...desc.material }
|
||
for (const key in matProps) {
|
||
if (key === 'type') continue
|
||
if (typeof matProps[key] === 'string' && THREE[matProps[key]] !== undefined) {
|
||
matProps[key] = THREE[matProps[key]]
|
||
}
|
||
}
|
||
// convert color strings like "0xffffaa" to numbers
|
||
if (typeof matProps.color === 'string' && matProps.color.startsWith('0x')) {
|
||
matProps.color = parseInt(matProps.color)
|
||
}
|
||
const mat = new THREE[matType](matProps)
|
||
|
||
|
||
obj = new THREE.Mesh(geom, mat)
|
||
if(desc.translate){
|
||
wrapper = new THREE.Object3D()
|
||
wrapper.add(obj)
|
||
obj.position.x = desc.translate[0]
|
||
obj.position.y = desc.translate[1]
|
||
obj.position.z = desc.translate[2]
|
||
}
|
||
|
||
} else if(desc.type === 'Group') {
|
||
obj = new THREE.Group()
|
||
} else {
|
||
throw new Error("Unknown type: " + desc.type)
|
||
}
|
||
|
||
// Apply transforms
|
||
if(desc.position) obj.position.set(...desc.position)
|
||
if(desc.rotation) obj.rotation.set(...desc.rotation)
|
||
if(desc.scale) obj.scale.set(...desc.scale)
|
||
|
||
// Recursively add children
|
||
if(desc.children) {
|
||
desc.children.forEach(childDesc => {
|
||
const childId = (childDesc.childSuffix) ? `${id}_${childDesc.childSuffix}` : ''
|
||
obj.add(this.agentFromJSON(childId, childDesc))
|
||
})
|
||
}
|
||
if(wrapper) obj=wrapper
|
||
obj.name = id
|
||
return obj
|
||
},
|
||
|
||
resizeRendererToDisplaySize() {
|
||
const width = this.canvasEl.clientWidth
|
||
const height = this.canvasEl.clientHeight
|
||
// Check if renderer size differs from displayed size
|
||
const needResize = this.canvasEl.width !== width || this.canvasEl.height !== height
|
||
if (needResize) {
|
||
// 1. Update renderer (base framebuffer)
|
||
this.renderer.setSize(width, height, false)
|
||
|
||
// 2️. Update camera aspect ratio
|
||
this.camera.aspect = width / height
|
||
this.camera.updateProjectionMatrix()
|
||
}
|
||
return needResize
|
||
},
|
||
|
||
cameraAutoFrame(object, camera, offset = 1.5, controls) {
|
||
const box = new THREE.Box3().setFromObject(object)
|
||
const size = new THREE.Vector3()
|
||
const center = new THREE.Vector3()
|
||
box.getSize(size)
|
||
box.getCenter(center)
|
||
|
||
const maxDim = Math.max(size.x, size.y, size.z)
|
||
const fov = camera.fov * Math.PI / 180
|
||
let cameraZ = maxDim / (2 * Math.tan(fov / 2)) * offset
|
||
|
||
camera.position.copy(center)
|
||
camera.position.z += cameraZ
|
||
camera.lookAt(center)
|
||
|
||
if (controls) {
|
||
controls.target.copy(center)
|
||
controls.update()
|
||
}
|
||
},
|
||
|
||
getObjectCenter(object) {
|
||
object.updateWorldMatrix(true, true)
|
||
const box = new THREE.Box3().setFromObject(object)
|
||
const center = new THREE.Vector3()
|
||
box.getCenter(center) // world coords
|
||
return center
|
||
},
|
||
|
||
getObjectTopCenter(object, offset = 0.1) {
|
||
object.updateWorldMatrix(true, true)
|
||
const box = new THREE.Box3().setFromObject(object)
|
||
const center = new THREE.Vector3()
|
||
box.getCenter(center)
|
||
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(object.quaternion).normalize()
|
||
const height = box.max.y - box.min.y
|
||
const top = center.clone().addScaledVector(up, height / 2 + offset)
|
||
return top
|
||
},
|
||
|
||
|
||
outlineMaterial: new THREE.MeshBasicMaterial({
|
||
color: 0xFFFFFF,
|
||
side: THREE.BackSide,
|
||
transparent: true,
|
||
opacity: 0.8,
|
||
depthTest: true,
|
||
depthWrite: false,
|
||
stencilWrite: true,
|
||
stencilFunc: THREE.AlwaysStencilFunc,
|
||
stencilRef: 1,
|
||
stencilMask: 0xff,
|
||
stencilFail: THREE.KeepStencilOp,
|
||
stencilZFail: THREE.KeepStencilOp,
|
||
stencilZPass: THREE.ReplaceStencilOp
|
||
}),
|
||
|
||
normalStencilMat: new THREE.MeshBasicMaterial({
|
||
color: 0xffffff,
|
||
stencilWrite: true,
|
||
stencilRef: 0,
|
||
stencilFunc: THREE.NotEqualStencilFunc,
|
||
stencilFail: THREE.KeepStencilOp,
|
||
stencilZFail: THREE.KeepStencilOp,
|
||
stencilZPass: THREE.KeepStencilOp
|
||
}),
|
||
|
||
highlight3DObj(obj, scene) {
|
||
this.highlightedObj = obj
|
||
// Scaled up Mesh to stencil
|
||
obj.traverse(child => {
|
||
if (child.isMesh) {
|
||
const highlightInflatedMesh = new THREE.Mesh(child.geometry, this.outlineMaterial)
|
||
highlightInflatedMesh.position.copy(child.getWorldPosition(new THREE.Vector3()))
|
||
highlightInflatedMesh.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()))
|
||
highlightInflatedMesh.scale.copy(child.getWorldScale(new THREE.Vector3())).multiplyScalar(1.05)
|
||
|
||
scene.add(highlightInflatedMesh)
|
||
child._outlineMesh = highlightInflatedMesh
|
||
}
|
||
})
|
||
|
||
// Normal Mesh render only if stencil != 1
|
||
obj.traverse(child => {
|
||
if (child.isMesh) {
|
||
child._savedMaterial = child.material
|
||
child.material = this.normalStencilMat
|
||
}
|
||
})
|
||
},
|
||
|
||
clearHighlight3DObj(obj,scene) {
|
||
this.highlightedObj = null
|
||
obj.traverse(child => {
|
||
if (child._outlineMesh) {
|
||
scene.remove(child._outlineMesh)
|
||
child._outlineMesh.geometry.dispose()
|
||
child._outlineMesh.material.dispose()
|
||
delete child._outlineMesh
|
||
}
|
||
|
||
if (child._savedMaterial) {
|
||
child.material = child._savedMaterial
|
||
delete child._savedMaterial
|
||
}
|
||
})
|
||
},
|
||
|
||
animateHighlight3DObj() {
|
||
if(!this.highlightedObj) return
|
||
const t = performance.now() * 0.002
|
||
const scale = 1.05 + Math.sin(t * 2) * 0.03
|
||
this.highlightedObj.traverse(child => {
|
||
if (child._outlineMesh) {
|
||
child._outlineMesh.scale.set(scale, scale, scale)
|
||
}
|
||
})
|
||
},
|
||
|
||
makePivotAtGeomCenter(object, scene) {
|
||
object.updateWorldMatrix(true, true)
|
||
const box = new THREE.Box3().setFromObject(object)
|
||
const centerW = box.getCenter(new THREE.Vector3())
|
||
|
||
// Create pivot at world center of the object
|
||
const pivot = new THREE.Object3D()
|
||
pivot.position.copy(centerW)
|
||
pivot.matrixAutoUpdate = true
|
||
scene.add(pivot)
|
||
|
||
// Compute the center in the object's local space
|
||
const centerLocal = object.worldToLocal(centerW.clone())
|
||
|
||
// Shift the object so its center sits at its own origin
|
||
object.position.sub(centerLocal)
|
||
|
||
// Reparent under the pivot (world pose preserved)
|
||
pivot.add(object)
|
||
|
||
return pivot
|
||
},
|
||
|
||
getNamedParent(obj) {
|
||
while (obj && ((!obj.name) || (obj.name.includes('_'))) ) {
|
||
obj = obj.parent
|
||
}
|
||
return obj
|
||
},
|
||
|
||
createArrow(origin, vector, options){
|
||
options.name = options.name || ''
|
||
options.color = options.color || 0xffaa00
|
||
options.headLength = options.headLength || 0.25
|
||
options.headWidth = options.headWidth || 0.1
|
||
options.scale = options.scale || 1
|
||
//const from = new THREE.Vector3(origin.x, origin.y, origin.z)
|
||
const dir = new THREE.Vector3(vector.x, vector.y, vector.z).normalize()
|
||
let length = Math.sqrt(vector.x**2 + vector.y**2 + vector.z**2)
|
||
if(length==0) return(null)
|
||
length *= options.scale
|
||
const arrow = new THREE.ArrowHelper(dir, origin, length, options.color, options.headLength, options.headWidth)
|
||
|
||
// Optional: add a subtle tube body for style
|
||
const shaftGeo = new THREE.CylinderGeometry(0.02, 0.02, length - options.headLength, 16)
|
||
const shaftMat = new THREE.MeshStandardMaterial({ color: options.color, metalness: 0.3, roughness: 0.2 })
|
||
const shaft = new THREE.Mesh(shaftGeo, shaftMat)
|
||
shaft.position.copy(origin.clone().add(dir.clone().multiplyScalar((length - options.headLength) / 2)))
|
||
shaft.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir)
|
||
|
||
const group = new THREE.Group()
|
||
group.add(arrow)
|
||
group.add(shaft)
|
||
if(options.name) group.name = options.name
|
||
this.scene.add(group)
|
||
return group
|
||
},
|
||
}
|