Add hex holder designer page (/holder)
Server-side OpenSCAD renders STL from bundled hex_cell.scad with parameter
overrides via -D. Frontend is a Three.js viewer with auto-form generated
from /api/holder/params. 'Design busbars →' button posts the computed
cell coordinates to /api/projects and redirects to the busbar editor with
the holder cells pre-loaded.
- holder.py: openscad subprocess wrapper + compute_cells()
(Python mirror of get_hex_center_points_*)
- scad/hex_cell.scad: verbatim copy of Addy/Hex-Cell-Holder source
- app.py: /holder route + /api/holder/{params,render,cells}
- static/holder.html etc: parameter form + Three.js STL viewer
- Dockerfile / install.sh: apt install openscad
- static/index.html: nav link Holder ↔ Busbars in topbar
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/* holder-viewer.js — Three.js scene for STL preview.
|
||||
*
|
||||
* Exports a single global `HolderViewer` (UMD-ish, attached to window) so the
|
||||
* non-module app script can use it. Set up via HolderViewer.init(canvasEl);
|
||||
* load STL bytes via HolderViewer.loadSTL(arrayBuffer).
|
||||
*/
|
||||
|
||||
import * as THREE from "three";
|
||||
import { STLLoader } from "three/addons/loaders/STLLoader.js";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
const HolderViewer = (() => {
|
||||
let scene, camera, renderer, controls;
|
||||
let mesh = null;
|
||||
let host = null;
|
||||
|
||||
function init(hostEl) {
|
||||
host = hostEl;
|
||||
const w = host.clientWidth || 800;
|
||||
const h = host.clientHeight || 600;
|
||||
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a0d12);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 5000);
|
||||
camera.position.set(120, 120, 120);
|
||||
camera.up.set(0, 0, 1); // Z-up (CAD convention)
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(w, h);
|
||||
host.innerHTML = "";
|
||||
host.appendChild(renderer.domElement);
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.screenSpacePanning = true;
|
||||
|
||||
// Lights
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
|
||||
const key = new THREE.DirectionalLight(0xffffff, 0.7);
|
||||
key.position.set(1, 1, 2).normalize();
|
||||
scene.add(key);
|
||||
const fill = new THREE.DirectionalLight(0xaccfff, 0.35);
|
||||
fill.position.set(-1, -1, 0.5).normalize();
|
||||
scene.add(fill);
|
||||
|
||||
// Build plate grid (10 mm minor, 50 mm major) at Z=0
|
||||
const grid = new THREE.GridHelper(400, 40, 0x2a3140, 0x1f2530);
|
||||
grid.rotateX(Math.PI / 2); // make it the XY plane (camera up is Z)
|
||||
scene.add(grid);
|
||||
|
||||
// Axes (small)
|
||||
const axes = new THREE.AxesHelper(20);
|
||||
scene.add(axes);
|
||||
|
||||
// Resize observer
|
||||
new ResizeObserver(_onResize).observe(host);
|
||||
|
||||
_animate();
|
||||
}
|
||||
|
||||
function _onResize() {
|
||||
if (!host) return;
|
||||
const w = host.clientWidth, h = host.clientHeight;
|
||||
if (w === 0 || h === 0) return;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
}
|
||||
|
||||
function _animate() {
|
||||
requestAnimationFrame(_animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
/** Load STL bytes (ArrayBuffer) and replace any existing mesh. */
|
||||
function loadSTL(buf) {
|
||||
const loader = new STLLoader();
|
||||
const geom = loader.parse(buf);
|
||||
geom.computeVertexNormals();
|
||||
// Centre the geometry on its bounding box and align bottom to Z=0.
|
||||
geom.computeBoundingBox();
|
||||
const bb = geom.boundingBox;
|
||||
const cx = (bb.min.x + bb.max.x) / 2;
|
||||
const cy = (bb.min.y + bb.max.y) / 2;
|
||||
geom.translate(-cx, -cy, -bb.min.z);
|
||||
|
||||
if (mesh) {
|
||||
scene.remove(mesh);
|
||||
mesh.geometry.dispose();
|
||||
mesh.material.dispose();
|
||||
}
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: 0xf08a24,
|
||||
metalness: 0.15,
|
||||
roughness: 0.65,
|
||||
flatShading: false,
|
||||
});
|
||||
mesh = new THREE.Mesh(geom, mat);
|
||||
scene.add(mesh);
|
||||
|
||||
_fitCameraToMesh(geom);
|
||||
}
|
||||
|
||||
function _fitCameraToMesh(geom) {
|
||||
const bb = geom.boundingBox;
|
||||
const size = bb.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const dist = maxDim * 1.6;
|
||||
camera.position.set(dist * 0.8, -dist * 0.8, dist * 0.8);
|
||||
controls.target.set(0, 0, size.z / 2);
|
||||
camera.lookAt(controls.target);
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (mesh) {
|
||||
scene.remove(mesh);
|
||||
mesh.geometry.dispose();
|
||||
mesh.material.dispose();
|
||||
mesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { init, loadSTL, clear };
|
||||
})();
|
||||
|
||||
window.HolderViewer = HolderViewer;
|
||||
Reference in New Issue
Block a user