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 }, }