1import * as THREE from "three"; 2 3export function frameObject(object, camera, controls) { 4 const box = computePreviewBox(object); 5 if (box.isEmpty()) return; 6 const center = box.getCenter(new THREE.Vector3()); 7 const size = box.getSize(new THREE.Vector3()); 8 const vFov = THREE.MathUtils.degToRad(camera.fov); 9 const hFov = 2 * Math.atan(Math.tan(vFov / 2) * (camera.aspect || 1)); 10 const tanV = Math.tan(vFov / 2); 11 const tanH = Math.tan(hFov / 2); 12 const direction = new THREE.Vector3(1, 0.75, 1).normalize(); 13 const corners = [ 14 new THREE.Vector3(box.min.x, box.min.y, box.min.z), 15 new THREE.Vector3(box.min.x, box.min.y, box.max.z), 16 new THREE.Vector3(box.min.x, box.max.y, box.min.z), 17 new THREE.Vector3(box.min.x, box.max.y, box.max.z), 18 new THREE.Vector3(box.max.x, box.min.y, box.min.z), 19 new THREE.Vector3(box.max.x, box.min.y, box.max.z), 20 new THREE.Vector3(box.max.x, box.max.y, box.min.z), 21 new THREE.Vector3(box.max.x, box.max.y, box.max.z), 22 ]; 23 let distance = Math.max(size.x, size.y, size.z, 0.35); 24 for (let pass = 0; pass < 4; pass += 1) { 25 camera.position.copy(center).addScaledVector(direction, distance); 26 camera.lookAt(center); 27 camera.updateMatrixWorld(true); 28 let extra = 0; 29 for (const corner of corners) { 30 const p = corner.clone().applyMatrix4(camera.matrixWorldInverse); 31 const neededDepth = Math.max(Math.abs(p.x) / Math.max(tanH, 0.001), Math.abs(p.y) / Math.max(tanV, 0.001)); 32 extra = Math.max(extra, neededDepth - (-p.z)); 33 } 34 if (extra <= 0.0001) break; 35 distance += extra; 36 } 37 distance *= 1.14; 38 controls.target.copy(center); 39 camera.position.copy(center).addScaledVector(direction, distance); 40 camera.lookAt(center); 41 camera.near = Math.max(distance / 1000, 0.001); 42 camera.far = Math.max(distance * 100, 2000); 43 camera.updateProjectionMatrix(); 44 controls.update(); 45} 46 47export function computePreviewBox(object) { 48 object.updateMatrixWorld(true); 49 const box = new THREE.Box3(); 50 object.traverse((node) => { 51 if (!node.isMesh) return; 52 node.frustumCulled = false; 53 if (node.geometry) { 54 if (!node.geometry.boundingBox) node.geometry.computeBoundingBox(); 55 if (!node.geometry.boundingSphere) node.geometry.computeBoundingSphere(); 56 if (node.geometry.boundingBox) { 57 box.union(node.geometry.boundingBox.clone().applyMatrix4(node.matrixWorld)); 58 } 59 } 60 }); 61 return box.isEmpty() ? new THREE.Box3().setFromObject(object) : box; 62} 63 64export function colorLuminance(color) { 65 if (!color) return 1; 66 return (color.r * 0.2126) + (color.g * 0.7152) + (color.b * 0.0722); 67} 68 69export function textureHasImage(texture) { 70 if (!texture) return false; 71 const image = texture.image || texture.source?.data; 72 if (!image) return false; 73 if (image.data) return true; 74 if (typeof image.naturalWidth === "number") return image.naturalWidth > 0; 75 if (typeof image.videoWidth === "number") return image.videoWidth > 0; 76 return (image.width || 0) > 0 && (image.height || 0) > 0; 77} 78 79export function prepareTexture(texture, colorTexture) { 80 if (!texture || !textureHasImage(texture)) return null; 81 if (colorTexture) texture.colorSpace = THREE.SRGBColorSpace; 82 texture.anisotropy = 4; 83 texture.needsUpdate = true; 84 return texture; 85} 86 87function usableUvAttribute(attribute) { 88 if (!attribute || !attribute.count || attribute.count < 3) return false; 89 const sampleCount = Math.min(attribute.count, 512);
90 const step = Math.max(1, Math.floor(attribute.count / sampleCount)); 91 let seen = 0; 92 let minU = Infinity, minV = Infinity, maxU = -Infinity, maxV = -Infinity; 93 for (let i = 0; i < attribute.count; i += step) { 94 const u = attribute.getX(i); 95 const v = attribute.getY(i); 96 if (!Number.isFinite(u) || !Number.isFinite(v)) continue; 97 seen += 1; 98 minU = Math.min(minU, u); 99 minV = Math.min(minV, v); 100 maxU = Math.max(maxU, u); 101 maxV = Math.max(maxV, v); 102 } 103 return seen >= 3 && (Math.abs(maxU - minU) > 0.00001 || Math.abs(maxV - minV) > 0.00001); 104} 105 106function generatedPlanarUvs(geometry) { 107 const position = geometry?.attributes?.position; 108 if (!position || !position.count) return null; 109 geometry.computeBoundingBox(); 110 const box = geometry.boundingBox; 111 if (!box || box.isEmpty()) return null; 112 const size = box.getSize(new THREE.Vector3()); 113 const axes = [ 114 { key: "x", size: Math.abs(size.x) }, 115 { key: "y", size: Math.abs(size.y) }, 116 { key: "z", size: Math.abs(size.z) }, 117 ].sort((a, b) => b.size - a.size); 118 const uAxis = axes[0].key; 119 const vAxis = axes[1].key; 120 const uSize = axes[0].size || 1; 121 const vSize = axes[1].size || 1; 122 const uMin = box.min[uAxis]; 123 const vMin = box.min[vAxis]; 124 const values = new Float32Array(position.count * 2); 125 const p = new THREE.Vector3(); 126 for (let i = 0; i < position.count; i += 1) { 127 p.fromBufferAttribute(position, i); 128 values[i * 2] = (p[uAxis] - uMin) / uSize; 129 values[i * 2 + 1] = (p[vAxis] - vMin) / vSize; 130 } 131 return new THREE.BufferAttribute(values, 2); 132} 133 134export function ensureTextureUvAttributes(geometry, { needsUv = true, needsUv2 = false, allowPlanarProjection = false } = {}) { 135 const result = { primary: "none", secondary: "none" }; 136 if (!geometry?.attributes) return result; 137 138 if (needsUv) { 139 if (usableUvAttribute(geometry.attributes.uv)) { 140 result.primary = "existing"; 141 } else if (usableUvAttribute(geometry.attributes.uv2)) { 142 geometry.setAttribute("uv", geometry.attributes.uv2.clone()); 143 result.primary = "copied-uv2"; 144 } else if (allowPlanarProjection) { 145 const generated = generatedPlanarUvs(geometry); 146 if (generated) { 147 geometry.setAttribute("uv", generated); 148 result.primary = "generated-planar"; 149 } 150 } 151 } 152 153 if (needsUv2) { 154 if (usableUvAttribute(geometry.attributes.uv2)) { 155 result.secondary = "existing"; 156 } else if (usableUvAttribute(geometry.attributes.uv)) { 157 geometry.setAttribute("uv2", geometry.attributes.uv.clone()); 158 result.secondary = "copied-uv"; 159 } 160 } 161 162 return result; 163} 164 165export function computeModelStats(object) { 166 let meshes = 0, tris = 0, vertices = 0, bones = 0, skinned = 0; 167 const materials = new Map(); 168 object.traverse((node) => { 169 if (node.isBone) bones += 1; 170 if (!node.isMesh || !node.geometry) return; 171 meshes += 1; 172 if (node.isSkinnedMesh) skinned += 1; 173 const g = node.geometry; 174 const count = g.index ? g.index.count : (g.attributes.position ? g.attributes.position.count : 0); 175 if (g.index) tris += count / 3; 176 else tris += count / 3; 177 if (g.attributes.position) vertices += g.attributes.position.count; 178 const mats = Array.isArray(node.material) ? node.material : [node.material]; 179 mats.forEach((m) => { 180 if (!m) return; 181 const key = m.uuid; 182 if (!materials.has(key)) { 183 materials.set(key, { 184 name: m.name || "(unnamed)", 185 color: m.userData?.previewColor || (m.color ? `#${m.color.getHexString()}` : "#888"), 186 hasMap: !!m.map, 187 }); 188 } 189 }); 190 }); 191 return { 192 meshes, 193 triangles: Math.round(tris), 194 vertices, 195 materials: Array.from(materials.values()), 196 materialCount: materials.size, 197 bones, 198 skinnedMeshes: skinned, 199 }; 200}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.