voronstl/web/lab.html

202 lines
5 KiB
HTML
Raw Normal View History

2026-02-28 16:33:16 -08:00
<!DOCTYPE html>
2026-03-04 17:47:07 -08:00
<!-- To run, start a local server:
python3 -m http.server 8001
Then open http://localhost:8001/lab.html in a modern browser.
-->
2026-02-28 16:33:16 -08:00
<html>
<head>
<meta charset="utf-8">
2026-03-04 17:47:07 -08:00
<title>RenderLab: PNG Profile Exporter</title>
2026-02-28 16:33:16 -08:00
<style>
body { margin:0; background:white; }
canvas { display:block; }
</style>
<script type="importmap">
{
"imports": {
"three": "./vendor/three/build/three.module.js",
"three/addons/": "./vendor/three/addons/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import GUI from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(3,3,3);
const renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true, // <-- critical for toDataURL on many setups
alpha: false // ensure opaque canvas
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xffffff, 1); // opaque white
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
const light = new THREE.DirectionalLight(0xffffff, 1.5);
light.position.set(5,5,5);
scene.add(light);
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
let mesh, edgeLines;
const loader = new GLTFLoader();
2026-03-04 17:47:07 -08:00
//
// This part is particularly tricky to view, so it makes an excellent test case.
//
// ./Voron-Stealthburner/STLs/Stealthburner/[o]_stealthburner_LED_carrier.stl.glb
//
loader.load('[o]_stealthburner_LED_carrier.stl.glb', gltf => {
2026-02-28 16:33:16 -08:00
mesh = gltf.scene.children[0];
mesh.material = new THREE.MeshStandardMaterial({
color: 0xdddddd,
roughness: 0.9,
metalness: 0
});
scene.add(mesh);
addEdges(30);
});
function ts_yyyymmdd_hhmiss () {
const d = new Date();
const pad = n => String(n).padStart(2,'0');
return (
d.getFullYear() +
pad(d.getMonth()+1) +
pad(d.getDate()) + '_' +
pad(d.getHours()) +
pad(d.getMinutes()) +
pad(d.getSeconds())
);
}
function downloadTextFile (filename, text) {
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function addEdges(angle) {
if (!mesh) return;
if (edgeLines) scene.remove(edgeLines);
const edges = new THREE.EdgesGeometry(mesh.geometry, angle);
edgeLines = new THREE.LineSegments(
edges,
new THREE.LineBasicMaterial({ color: 0x000000 })
);
scene.add(edgeLines);
}
const params = {
wireframe: false,
edgeAngle: 30,
lightIntensity: 1.5,
savePNG: () => {
renderer.render(scene, camera); // recommended before capture
const link = document.createElement('a');
link.download = 'render.png';
link.href = renderer.domElement.toDataURL("image/png");
link.click();
},
exportProfile: () => {
controls.update(); // ensure target is current
const profile = {
provenance: `lab.html exportProfile ${new Date().toString()}`,
output: {
width: renderer.domElement.width,
height: renderer.domElement.height,
pixelRatio: renderer.getPixelRatio()
},
scene: {
background: scene.background?.getHex?.() ?? null
},
camera: {
type: 'PerspectiveCamera',
fov: camera.fov,
near: camera.near,
far: camera.far,
position: camera.position.toArray(),
up: camera.up.toArray()
},
controls: {
target: controls.target.toArray()
},
renderParams: {
wireframe: params.wireframe,
edgeAngle: params.edgeAngle,
lightIntensity: params.lightIntensity
},
lights: {
directional: { position: light.position.toArray(), intensity: light.intensity },
ambient: { intensity: ambient.intensity }
}
};
const fn = `three_profile_${ts_yyyymmdd_hhmiss()}.json`;
downloadTextFile(fn, JSON.stringify(profile, null, 2));
console.log('Exported profile:', profile);
}
};
const gui = new GUI();
gui.add(params, 'wireframe').onChange(v => {
if (mesh) mesh.material.wireframe = v;
});
gui.add(params, 'edgeAngle', 1, 90).onChange(v => addEdges(v));
gui.add(params, 'lightIntensity', 0, 3).onChange(v => light.intensity = v);
gui.add(params, 'savePNG');
gui.add(params, 'exportProfile');
window.addEventListener('resize', () => {
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>