1
0
Fork 0
mirror of https://github.com/imjasonh/scad-to-png synced 2026-07-07 01:22:23 +00:00
scad-to-png/stl-renderer.mjs

175 lines
5 KiB
JavaScript
Raw Permalink Normal View History

import { createCanvas } from '@napi-rs/canvas';
// Simple STL parser
function parseSTL(buffer) {
const dataView = new DataView(buffer.buffer || buffer);
const isASCII = new TextDecoder().decode(buffer.slice(0, 6)) === 'solid ';
if (isASCII) {
return parseASCIISTL(new TextDecoder().decode(buffer));
} else {
return parseBinarySTL(dataView);
}
}
function parseASCIISTL(text) {
const triangles = [];
const lines = text.split('\n');
let i = 0;
while (i < lines.length) {
const line = lines[i].trim();
if (line.startsWith('facet normal')) {
const vertices = [];
i += 2; // skip 'outer loop'
for (let j = 0; j < 3; j++) {
const vertexLine = lines[i + j].trim();
const coords = vertexLine.split(/\s+/).slice(1).map(parseFloat);
vertices.push(coords);
}
triangles.push(vertices);
i += 5; // skip to next facet
} else {
i++;
}
}
return triangles;
}
function parseBinarySTL(dataView) {
const triangles = [];
const triangleCount = dataView.getUint32(80, true);
let offset = 84;
for (let i = 0; i < triangleCount; i++) {
// Skip normal (12 bytes)
offset += 12;
const vertices = [];
for (let j = 0; j < 3; j++) {
vertices.push([
dataView.getFloat32(offset, true),
dataView.getFloat32(offset + 4, true),
dataView.getFloat32(offset + 8, true)
]);
offset += 12;
}
triangles.push(vertices);
offset += 2; // Skip attribute byte count
}
return triangles;
}
// 3D to 2D projection
function project3Dto2D(vertex, camera, width, height) {
// Apply camera transformation
const dx = vertex[0] - camera.target[0];
const dy = vertex[1] - camera.target[1];
const dz = vertex[2] - camera.target[2];
// Rotate around Y axis (horizontal rotation)
const cosY = Math.cos(camera.rotation[1]);
const sinY = Math.sin(camera.rotation[1]);
const x1 = dx * cosY - dz * sinY;
const z1 = dx * sinY + dz * cosY;
// Rotate around X axis (vertical rotation)
const cosX = Math.cos(camera.rotation[0]);
const sinX = Math.sin(camera.rotation[0]);
const y1 = dy * cosX - z1 * sinX;
const z2 = dy * sinX + z1 * cosX + camera.distance;
// Perspective projection
if (z2 <= 0) return null;
const scale = camera.scale / z2;
const x2d = x1 * scale + width / 2;
const y2d = -y1 * scale + height / 2;
return [x2d, y2d, z2];
}
// Simple z-buffer renderer
export function renderSTL(stlBuffer, viewConfig, width, height) {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// Parse STL
const triangles = parseSTL(stlBuffer);
if (triangles.length === 0) {
throw new Error('No triangles found in STL');
}
// Calculate bounds
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
triangles.forEach(triangle => {
triangle.forEach(vertex => {
minX = Math.min(minX, vertex[0]);
minY = Math.min(minY, vertex[1]);
minZ = Math.min(minZ, vertex[2]);
maxX = Math.max(maxX, vertex[0]);
maxY = Math.max(maxY, vertex[1]);
maxZ = Math.max(maxZ, vertex[2]);
});
});
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const centerZ = (minZ + maxZ) / 2;
const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
// Set up camera based on view config
const camera = {
target: [centerX, centerY, centerZ],
distance: size * viewConfig.distance * 0.9,
scale: Math.min(width, height) * 1.2,
rotation: [
Math.atan2(viewConfig.position[1], Math.sqrt(viewConfig.position[0]**2 + viewConfig.position[2]**2)),
Math.atan2(viewConfig.position[0], viewConfig.position[2])
]
};
// Clear canvas
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, width, height);
// Sort triangles by average Z (painter's algorithm)
const projectedTriangles = triangles.map(triangle => {
const projected = triangle.map(v => project3Dto2D(v, camera, width, height));
if (projected.some(p => p === null)) return null;
const avgZ = projected.reduce((sum, p) => sum + p[2], 0) / 3;
return { triangle, projected, avgZ };
}).filter(t => t !== null);
projectedTriangles.sort((a, b) => b.avgZ - a.avgZ);
// Render triangles
projectedTriangles.forEach(({ projected }) => {
ctx.beginPath();
ctx.moveTo(projected[0][0], projected[0][1]);
ctx.lineTo(projected[1][0], projected[1][1]);
ctx.lineTo(projected[2][0], projected[2][1]);
ctx.closePath();
// Simple shading based on Z depth
const avgZ = (projected[0][2] + projected[1][2] + projected[2][2]) / 3;
const brightness = Math.max(0.3, Math.min(1, 1 - (avgZ - camera.distance) / (size * 2)));
const color = Math.floor(brightness * 200);
ctx.fillStyle = `rgb(${color}, ${color}, ${color + 20})`;
ctx.fill();
ctx.strokeStyle = `rgb(${color - 20}, ${color - 20}, ${color})`;
ctx.lineWidth = 0.5;
ctx.stroke();
});
return canvas.toBuffer('image/png');
}