(anchorBtn)
| 14785 | maxDim > 0 && tanHalfFov > 0 |
| 14786 | ? Math.abs(maxDim / 2 / tanHalfFov) * 1.5 |
| 14787 | : 5; |
| 14788 | if (!Number.isFinite(cameraZ) || cameraZ <= 0) cameraZ = 5; |
| 14789 | |
| 14790 | // Camera sits on the (z,z,z) diagonal — keep far plane beyond that distance. |
| 14791 | const cameraDistance = cameraZ * Math.sqrt(3); |
| 14792 | camera.near = Math.max(0.01, cameraDistance / 1000); |
| 14793 | camera.far = Math.max(10000, cameraDistance * 20); |
| 14794 | camera.updateProjectionMatrix(); |
| 14795 | |
| 14796 | camera.position.set(cameraZ, cameraZ, cameraZ); |
| 14797 | camera.lookAt(0, 0, 0); |
| 14798 | |
| 14799 | renderer.render(scene, camera); |
| 14800 | } |
| 14801 | |
| 14802 | /** True when a thumbnail data-URL is effectively empty (transparent / clipped render). */ |
| 14803 | function isMostlyEmptyThumbnailDataUrl(dataUrl) { |
| 14804 | return new Promise((resolve) => { |
| 14805 | if (!dataUrl || typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image')) { |
| 14806 | resolve(true); |
| 14807 | return; |
| 14808 | } |
| 14809 | const img = new Image(); |
| 14810 | img.onload = () => { |
| 14811 | try { |
| 14812 | const w = Math.min(64, img.width || 64); |
| 14813 | const h = Math.min(64, img.height || 64); |
| 14814 | const canvas = document.createElement('canvas'); |
| 14815 | canvas.width = w; |
| 14816 | canvas.height = h; |
| 14817 | const ctx = canvas.getContext('2d', { willReadFrequently: true }); |
| 14818 | if (!ctx) { |
| 14819 | resolve(false); |
| 14820 | return; |
| 14821 | } |
| 14822 | ctx.drawImage(img, 0, 0, w, h); |
| 14823 | const pixels = ctx.getImageData(0, 0, w, h).data; |
| 14824 | let opaque = 0; |
| 14825 | for (let i = 0; i < pixels.length; i += 4) { |
| 14826 | if (pixels[i + 3] > 12) opaque += 1; |
| 14827 | } |
| 14828 | // Less than 0.5% opaque pixels ≈ blank (model clipped or failed). |
| 14829 | resolve(opaque < w * h * 0.005); |
| 14830 | } catch (error) { |
| 14831 | resolve(false); |
| 14832 | } |
| 14833 | }; |
| 14834 | img.onerror = () => resolve(true); |
no test coverage detected