consolidate stl/obj load/encode into src/load classes

This commit is contained in:
Stewart Allen 2025-12-23 21:20:11 -05:00
commit 69ef0ff54b
4 changed files with 119 additions and 135 deletions

View file

@ -4,6 +4,8 @@ import { api } from './api.js';
import { space } from '../../moto/space.js';
import { THREE } from '../../ext/three.js';
import { tool as MeshTool } from '../../mesh/tool.js';
import { encode as objEncode } from '../../load/obj.js';
import { encode as stlEncode } from '../../load/stl.js';
const selectedMeshes = [];
@ -258,61 +260,33 @@ function exportWidgets(format = "stl") {
facets += geo.attributes.position.count;
});
if (format === "obj") {
let obj = [];
let vpad = 0;
for (let out of outs) {
let recs = outs.map(out => {
let meta = out.widget.meta;
let name = meta.file || 'unnamed';
obj.push(`g ${name}`);
let { position } = out.geo.attributes;
let pvals = position.array;
for (let i=0, il=position.count; i<il; i += 3) {
let pi = i * position.itemSize;
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`f ${i+1+vpad} ${i+2+vpad} ${i+3+vpad}`);
}
vpad += position.count;
}
return obj.join('\n');
return { file: name, varr: position.array };
});
return objEncode(recs);
}
let stl = new Uint8Array(80 + 4 + facets/3 * 50);
let dat = new DataView(stl.buffer);
let pos = 84;
dat.setInt32(80, facets/3, true);
for (let out of outs) {
let recs = outs.map(out => {
let { position } = out.geo.attributes;
let pvals = position.array;
for (let i=0, il=position.count; i<il; i += 3) {
let pi = i * position.itemSize;
let p0 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let p1 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let p2 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let norm = THREE.computeFaceNormal(p0, p1, p2);
let xo = 0, yo = 0, zo = 0;
if (outs.length > 1) {
let {x, y, z} = out.widget.track.pos;
xo = x;
yo = y;
zo = z;
let varr = pvals;
// Apply position offset if exporting multiple widgets
if (outs.length > 1) {
let {x, y, z} = out.widget.track.pos;
varr = new Float32Array(pvals.length);
for (let i = 0; i < pvals.length; i += 3) {
varr[i] = pvals[i] + x;
varr[i+1] = pvals[i+1] + y;
varr[i+2] = pvals[i+2] + z;
}
dat.setFloat32(pos + 0, norm.x, true);
dat.setFloat32(pos + 4, norm.y, true);
dat.setFloat32(pos + 8, norm.z, true);
dat.setFloat32(pos + 12, p0.x + xo, true);
dat.setFloat32(pos + 16, p0.y + yo, true);
dat.setFloat32(pos + 20, p0.z + zo, true);
dat.setFloat32(pos + 24, p1.x + xo, true);
dat.setFloat32(pos + 28, p1.y + yo, true);
dat.setFloat32(pos + 32, p1.z + zo, true);
dat.setFloat32(pos + 36, p2.x + xo, true);
dat.setFloat32(pos + 40, p2.y + yo, true);
dat.setFloat32(pos + 44, p2.z + zo, true);
pos += 50;
}
}
return stl;
return { file: out.widget.meta.file || 'unnamed', varr };
});
return stlEncode(recs);
}
function widgets(orall) {

View file

@ -4,6 +4,28 @@
import { util } from '../geo/base.js';
/**
* Export vertex array(s) to OBJ format string
* @param {Array} recs - array of {file: string, varr: Float32Array} records
* @param {String} header - optional header comment
* @returns {String} OBJ format text
*/
export function encode(recs, header = '# Generated @ https://grid.space') {
let p = 1;
let obj = [header];
for (let rec of recs) {
let { file, varr } = rec;
obj.push(`g ${file || 'unnamed'}`);
for (let i=0; i<varr.length; p += 3) {
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`f ${p} ${p+1} ${p+2}`);
}
}
return obj.join('\n');
}
/**
* @param {String} text
* @returns {Array} vertex face array

View file

@ -1,6 +1,76 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { THREE } from '../ext/three.js';
const CDH = 'Content-Disposition';
const { Vector3, computeFaceNormal } = THREE;
/**
* Encode vertex array(s) to binary STL format
* Automatically computes normals from triangle vertices
* @param {Array} recs - array of {file: string, varr: Float32Array} records
* @param {String} header - optional header text (max 80 chars)
* @returns {Uint8Array} binary STL data
*/
export function encode(recs, header = '') {
// Calculate total vertices
let vtot = 0;
for (let rec of recs) {
vtot += (rec.varr.length / 3);
}
// Create STL buffer: 80 byte header + 4 byte count + (50 bytes per triangle)
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
let dat = new DataView(stl.buffer);
let pos = 84;
// Write header text (first 80 bytes)
if (header) {
header.substring(0, 80).split('').forEach((c, i) => {
dat.setUint8(i, c.charCodeAt(0));
});
}
// Write triangle count at byte 80
dat.setInt32(80, vtot/3, true);
// Write triangles
for (let rec of recs) {
let { varr } = rec;
for (let i = 0, l = varr.length; i < l;) {
// Read three vertices
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p1 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p2 = new Vector3(varr[i++], varr[i++], varr[i++]);
// Compute face normal
let norm = computeFaceNormal(p0, p1, p2);
// Write normal (3 floats)
dat.setFloat32(pos + 0, norm.x, true);
dat.setFloat32(pos + 4, norm.y, true);
dat.setFloat32(pos + 8, norm.z, true);
// Write vertices (9 floats)
dat.setFloat32(pos + 12, p0.x, true);
dat.setFloat32(pos + 16, p0.y, true);
dat.setFloat32(pos + 20, p0.z, true);
dat.setFloat32(pos + 24, p1.x, true);
dat.setFloat32(pos + 28, p1.y, true);
dat.setFloat32(pos + 32, p1.z, true);
dat.setFloat32(pos + 36, p2.x, true);
dat.setFloat32(pos + 40, p2.y, true);
dat.setFloat32(pos + 44, p2.z, true);
// Attribute byte count (2 bytes) - typically 0
dat.setUint16(pos + 48, 0, true);
pos += 50;
}
}
return stl;
}
export class STL {
constructor() {

View file

@ -17,6 +17,8 @@ import { CSG } from '../geo/csg.js';
import { util } from './util.js';
import { tool as meshTool } from './tool.js';
import { meshToSTEPWithFaces } from '../load/step.js';
import { encode as objEncode } from '../load/obj.js';
import { encode as stlEncode } from '../load/stl.js';
const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE;
@ -517,51 +519,9 @@ const model = {
}
switch (format) {
case "obj":
let p = 1;
let obj = [header];
for (let rec of recs) {
let { file, varr } = rec;
obj.push(`g ${file}`);
for (let i=0; i<varr.length; p += 3) {
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`f ${p} ${p+1} ${p+2}`);
}
}
return obj.join('\n');
return objEncode(recs, header);
case "stl":
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
let dat = new DataView(stl.buffer);
let pos = 84;
header.split('').forEach((c,i) => {
dat.setUint8(i, c.charCodeAt(0));
});
// todo put Mesh:Tool info in header
dat.setInt32(80, vtot/3, true);
for (let rec of recs) {
let { id, matrix, file, varr } = rec;
for (let i=0, l=varr.length; i<l;) {
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p1 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p2 = new Vector3(varr[i++], varr[i++], varr[i++]);
let norm = computeFaceNormal(p0, p1, p2);
dat.setFloat32(pos + 0, norm.x, true);
dat.setFloat32(pos + 4, norm.y, true);
dat.setFloat32(pos + 8, norm.z, true);
dat.setFloat32(pos + 12, p0.x, true);
dat.setFloat32(pos + 16, p0.y, true);
dat.setFloat32(pos + 20, p0.z, true);
dat.setFloat32(pos + 24, p1.x, true);
dat.setFloat32(pos + 28, p1.y, true);
dat.setFloat32(pos + 32, p1.z, true);
dat.setFloat32(pos + 36, p2.x, true);
dat.setFloat32(pos + 40, p2.y, true);
dat.setFloat32(pos + 44, p2.z, true);
pos += 50;
}
}
return stl;
return stlEncode(recs, header);
default:
throw `invalid format "${format}"`;
}
@ -617,51 +577,9 @@ const file = {
}
return meshToSTEPWithFaces(varr);
case "obj":
let p = 1;
let obj = [header];
for (let rec of recs) {
let { file, varr } = rec;
obj.push(`g ${file}`);
for (let i=0; i<varr.length; p += 3) {
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
obj.push(`f ${p} ${p+1} ${p+2}`);
}
}
return obj.join('\n');
return objEncode(recs, header);
case "stl":
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
let dat = new DataView(stl.buffer);
let pos = 84;
header.split('').forEach((c,i) => {
dat.setUint8(i, c.charCodeAt(0));
});
// todo put Mesh:Tool info in header
dat.setInt32(80, vtot/3, true);
for (let rec of recs) {
let { id, matrix, file, varr } = rec;
for (let i=0, l=varr.length; i<l;) {
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p1 = new Vector3(varr[i++], varr[i++], varr[i++]);
let p2 = new Vector3(varr[i++], varr[i++], varr[i++]);
let norm = computeFaceNormal(p0, p1, p2);
dat.setFloat32(pos + 0, norm.x, true);
dat.setFloat32(pos + 4, norm.y, true);
dat.setFloat32(pos + 8, norm.z, true);
dat.setFloat32(pos + 12, p0.x, true);
dat.setFloat32(pos + 16, p0.y, true);
dat.setFloat32(pos + 20, p0.z, true);
dat.setFloat32(pos + 24, p1.x, true);
dat.setFloat32(pos + 28, p1.y, true);
dat.setFloat32(pos + 32, p1.z, true);
dat.setFloat32(pos + 36, p2.x, true);
dat.setFloat32(pos + 40, p2.y, true);
dat.setFloat32(pos + 44, p2.z, true);
pos += 50;
}
}
return stl;
return stlEncode(recs, header);
default:
throw `invalid format "${format}"`;
}