(file)
| 15611 | } |
| 15612 | |
| 15613 | // NOTE: loadModel function is defined earlier in the file (around line 2840) |
| 15614 | // This duplicate has been removed to use the enhanced version that checks for embedded images |
| 15615 | |
| 15616 | function fitCameraToObject(camera, object, scene, renderer) { |
| 15617 | // Orient first, then measure — otherwise large flat STLs (e.g. 400×420×4 plates) |
| 15618 | // get framed for the wrong bbox and often land past the camera far plane. |
| 15619 | object.rotation.x = -Math.PI / 2; |
| 15620 | object.updateMatrixWorld(true); |
| 15621 | |
| 15622 | const boundingBox = new THREE.Box3().setFromObject(object); |
| 15623 | const size = boundingBox.getSize(new THREE.Vector3()); |
| 15624 | const center = boundingBox.getCenter(new THREE.Vector3()); |
| 15625 | |
| 15626 | if (Number.isFinite(center.x) && Number.isFinite(center.y) && Number.isFinite(center.z)) { |
| 15627 | object.position.sub(center); |
| 15628 | object.updateMatrixWorld(true); |
| 15629 | } |
| 15630 | |
| 15631 | const fittedBox = new THREE.Box3().setFromObject(object); |
| 15632 | const fittedSize = fittedBox.getSize(new THREE.Vector3()); |
| 15633 | const maxDim = Math.max(fittedSize.x, fittedSize.y, fittedSize.z, 1e-3); |
| 15634 | |
| 15635 | const fov = camera.fov * (Math.PI / 180); |
| 15636 | const tanHalfFov = Math.tan(fov / 2); |
| 15637 | let cameraZ = |
| 15638 | maxDim > 0 && tanHalfFov > 0 |
| 15639 | ? Math.abs(maxDim / 2 / tanHalfFov) * 1.5 |
| 15640 | : 5; |
| 15641 | if (!Number.isFinite(cameraZ) || cameraZ <= 0) cameraZ = 5; |
| 15642 | |
| 15643 | // Camera sits on the (z,z,z) diagonal — keep far plane beyond that distance. |
| 15644 | const cameraDistance = cameraZ * Math.sqrt(3); |
| 15645 | camera.near = Math.max(0.01, cameraDistance / 1000); |
| 15646 | camera.far = Math.max(10000, cameraDistance * 20); |
| 15647 | camera.updateProjectionMatrix(); |
| 15648 | |
| 15649 | camera.position.set(cameraZ, cameraZ, cameraZ); |
| 15650 | camera.lookAt(0, 0, 0); |
| 15651 | |
| 15652 | renderer.render(scene, camera); |
| 15653 | } |
| 15654 | |
| 15655 | /** True when a thumbnail data-URL is effectively empty (transparent / clipped render). */ |
| 15656 | function isMostlyEmptyThumbnailDataUrl(dataUrl) { |
| 15657 | return new Promise((resolve) => { |
| 15658 | if (!dataUrl || typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image')) { |
| 15659 | resolve(true); |
| 15660 | return; |
| 15661 | } |
| 15662 | const img = new Image(); |
| 15663 | img.onload = () => { |
| 15664 | try { |
| 15665 | const w = Math.min(64, img.width || 64); |
| 15666 | const h = Math.min(64, img.height || 64); |
| 15667 | const canvas = document.createElement('canvas'); |
| 15668 | canvas.width = w; |
| 15669 | canvas.height = h; |
| 15670 | const ctx = canvas.getContext('2d', { willReadFrequently: true }); |
no test coverage detected