Files
P42_UI/app/helpers/helpers3D.module.js
2025-11-17 20:52:38 +00:00

260 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
},
}