grid-apps-cmms/src/load/step.js
2025-10-26 10:53:41 -04:00

614 lines
23 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { tool as MeshTool } from '../mesh/tool.js';
const { Vector3 } = THREE;
/**
* Convert STL triangles to STEP file format
*
* @param {Array} triangles - Array of triangle objects with vertices: [{v1, v2, v3}, ...]
* where v1, v2, v3 are {x, y, z} objects or Vector3 instances
* @param {Object} options - Optional parameters
* @param {string} options.productName - Name for the STEP product (default: 'mesh')
* @param {string} options.units - Units: 'mm', 'cm', 'm', 'inch' (default: 'mm')
* @returns {string} STEP file content as a string
*/
export function meshToSTEP(triangles, options = {}) {
const productName = options.productName || 'mesh';
const units = options.units || 'mm';
// Build vertex and triangle topology
const vertices = [];
const vertexMap = new Map(); // Map vertex coords to index
const faces = [];
// Helper to find or add vertex
function getOrAddVertex(v) {
const vec = v instanceof Vector3 ? v : new Vector3(v.x, v.y, v.z);
// Check if vertex already exists
for (let i = 0; i < vertices.length; i++) {
if (vertices[i].equals(vec)) {
return i;
}
}
// Add new vertex
vertices.push(vec);
return vertices.length - 1;
}
// Process triangles
for (const tri of triangles) {
const idx0 = getOrAddVertex(tri.v1 || tri.vertices[0]);
const idx1 = getOrAddVertex(tri.v2 || tri.vertices[1]);
const idx2 = getOrAddVertex(tri.v3 || tri.vertices[2]);
faces.push([idx0, idx1, idx2]);
}
// Generate STEP file
let step = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('STL to STEP Conversion'),'2;1');
FILE_NAME('${productName}.step','${new Date().toISOString()}',(''),(''),'stl-to-step','','');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));
ENDSEC;
DATA;
`;
let id = 1;
// Units mapping
const unitMultiplier = {
'mm': '.MILLI.',
'cm': '.CENTI.',
'm': '',
'inch': '.INCH.'
};
const unitPrefix = unitMultiplier[units] || '.MILLI.';
// Units and context
const lengthUnit = id++;
const planeAngleUnit = id++;
const solidAngleUnit = id++;
step += `#${lengthUnit}=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(${unitPrefix},.METRE.));\n`;
step += `#${planeAngleUnit}=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));\n`;
step += `#${solidAngleUnit}=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());\n`;
const uncertainty = id++;
step += `#${uncertainty}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#${lengthUnit},'distance_accuracy_value','confusion accuracy');\n`;
// Geometric representation context
const geomContextId = id++;
step += `#${geomContextId}=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#${uncertainty}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#${lengthUnit},#${planeAngleUnit},#${solidAngleUnit}))REPRESENTATION_CONTEXT('ID1','3D'));\n`;
// Application context
const appContext = id++;
step += `#${appContext}=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');\n`;
const appProtocol = id++;
step += `#${appProtocol}=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',1998,#${appContext});\n`;
// Product definition
const productId = id++;
step += `#${productId}=PRODUCT('${productName}','${productName}','',(#${appContext}));\n`;
const productDefFormId = id++;
step += `#${productDefFormId}=PRODUCT_DEFINITION_FORMATION('','',#${productId});\n`;
const productDefCtx = id++;
step += `#${productDefCtx}=PRODUCT_DEFINITION_CONTEXT('part definition',#${appContext},'design');\n`;
const productDefId = id++;
step += `#${productDefId}=PRODUCT_DEFINITION('design','',#${productDefFormId},#${productDefCtx});\n`;
// Shape representation - reserve ID
const shapeRepId = id++;
// Create cartesian points for all vertices
const vertexIds = [];
for (let i = 0; i < vertices.length; i++) {
const v = vertices[i];
const pointId = id++;
step += `#${pointId}=CARTESIAN_POINT('',(${v.x.toFixed(6)},${v.y.toFixed(6)},${v.z.toFixed(6)}));\n`;
vertexIds.push(pointId);
}
// Create vertex points
const vertexPointIds = [];
for (let i = 0; i < vertices.length; i++) {
const vpId = id++;
step += `#${vpId}=VERTEX_POINT('',#${vertexIds[i]});\n`;
vertexPointIds.push(vpId);
}
// Advanced B-rep shape representation
const manifoldId = id++;
const closedShellId = id++;
step += `#${shapeRepId}=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#${manifoldId}),#${geomContextId});\n`;
step += `#${manifoldId}=MANIFOLD_SOLID_BREP('',#${closedShellId});\n`;
const faceIds = [];
for (let i = 0; i < faces.length; i++) {
faceIds.push(id++);
}
step += `#${closedShellId}=CLOSED_SHELL('',(${faceIds.map(fid => `#${fid}`).join(',')}));\n`;
// Create each triangular face
for (let faceIdx = 0; faceIdx < faces.length; faceIdx++) {
const faceVertices = faces[faceIdx];
const faceId = faceIds[faceIdx];
const faceOuterBoundId = id++;
const edgeLoopId = id++;
// Create edges for this triangular face
const orientedEdgeIds = [];
for (let i = 0; i < 3; i++) {
const v1Idx = faceVertices[i];
const v2Idx = faceVertices[(i + 1) % 3];
const orientedEdgeId = id++;
const edgeCurveId = id++;
const lineId = id++;
const vectorId = id++;
const directionId = id++;
orientedEdgeIds.push(orientedEdgeId);
// Direction vector
const v1 = vertices[v1Idx];
const v2 = vertices[v2Idx];
const dx = v2.x - v1.x;
const dy = v2.y - v1.y;
const dz = v2.z - v1.z;
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-10) {
console.warn(`Warning: Degenerate edge in face ${faceIdx}`);
continue;
}
step += `#${directionId}=DIRECTION('',(${(dx / len).toFixed(6)},${(dy / len).toFixed(6)},${(dz / len).toFixed(6)}));\n`;
step += `#${vectorId}=VECTOR('',#${directionId},${len.toFixed(6)});\n`;
step += `#${lineId}=LINE('',#${vertexIds[v1Idx]},#${vectorId});\n`;
step += `#${edgeCurveId}=EDGE_CURVE('',#${vertexPointIds[v1Idx]},#${vertexPointIds[v2Idx]},#${lineId},.T.);\n`;
step += `#${orientedEdgeId}=ORIENTED_EDGE('',*,*,#${edgeCurveId},.T.);\n`;
}
step += `#${edgeLoopId}=EDGE_LOOP('',(${orientedEdgeIds.map(eid => `#${eid}`).join(',')}));\n`;
step += `#${faceOuterBoundId}=FACE_OUTER_BOUND('',#${edgeLoopId},.T.);\n`;
// Create plane for face
const planeId = id++;
const axisPlacementId = id++;
const normalDirId = id++;
const refDirId = id++;
// Calculate face normal and reference direction
const v0 = vertices[faceVertices[0]];
const v1 = vertices[faceVertices[1]];
const v2 = vertices[faceVertices[2]];
const edge1 = v1.clone().sub(v0);
const edge2 = v2.clone().sub(v0);
const normal = edge1.clone().cross(edge2).normalize();
// Calculate face center for plane origin
const centerX = (v0.x + v1.x + v2.x) / 3;
const centerY = (v0.y + v1.y + v2.y) / 3;
const centerZ = (v0.z + v1.z + v2.z) / 3;
const centerPointId = id++;
step += `#${centerPointId}=CARTESIAN_POINT('',(${centerX.toFixed(6)},${centerY.toFixed(6)},${centerZ.toFixed(6)}));\n`;
step += `#${normalDirId}=DIRECTION('',(${normal.x.toFixed(6)},${normal.y.toFixed(6)},${normal.z.toFixed(6)}));\n`;
const refDir = edge1.normalize();
step += `#${refDirId}=DIRECTION('',(${refDir.x.toFixed(6)},${refDir.y.toFixed(6)},${refDir.z.toFixed(6)}));\n`;
step += `#${axisPlacementId}=AXIS2_PLACEMENT_3D('',#${centerPointId},#${normalDirId},#${refDirId});\n`;
step += `#${planeId}=PLANE('',#${axisPlacementId});\n`;
step += `#${faceId}=ADVANCED_FACE('',(#${faceOuterBoundId}),#${planeId},.T.);\n`;
}
// Product definition shape
const prodDefShapeId = id++;
step += `#${prodDefShapeId}=PRODUCT_DEFINITION_SHAPE('','',#${productDefId});\n`;
step += `#${id++}=SHAPE_DEFINITION_REPRESENTATION(#${prodDefShapeId},#${shapeRepId});\n`;
step += `ENDSEC;\nEND-ISO-10303-21;\n`;
return step;
}
/**
* Convert STL triangles to STEP file format with face merging
* Groups co-planar connected triangles into larger faces
*
* @param {Array} triangles - Array of triangle objects with vertices: [{v1, v2, v3}, ...]
* where v1, v2, v3 are {x, y, z} objects or Vector3 instances
* @param {Object} options - Optional parameters
* @param {string} options.productName - Name for the STEP product (default: 'mesh')
* @param {string} options.units - Units: 'mm', 'cm', 'm', 'inch' (default: 'mm')
* @param {number} options.angleTolerance - Angle tolerance in degrees for co-planarity (default: 1)
* @returns {string} STEP file content as a string
*/
export function meshToSTEPWithFaces(triangles, options = {}) {
const productName = options.productName || 'mesh';
const units = options.units || 'mm';
const angleTolerance = options.angleTolerance || 1;
const radianTolerance = angleTolerance * (Math.PI / 180);
// Convert triangles to flat vertex array for MeshTool
const vertices = [];
for (const tri of triangles) {
const v1 = tri.v1 || tri.vertices[0];
const v2 = tri.v2 || tri.vertices[1];
const v3 = tri.v3 || tri.vertices[2];
vertices.push(
v1.x, v1.y, v1.z,
v2.x, v2.y, v2.z,
v3.x, v3.y, v3.z
);
}
// Use MeshTool to index faces and find adjacency
const tool = new MeshTool();
tool.index(vertices);
const totalFaces = triangles.length;
const processed = {};
const surfaces = [];
// Find all connected surfaces
for (let i = 0; i < totalFaces; i++) {
if (processed[i]) continue;
// Find all connected co-planar faces starting from this face
const connectedFaces = tool.findConnectedSurface([i], radianTolerance, undefined, processed);
if (connectedFaces.length > 0) {
// Generate outline(s) for this surface
const outlines = tool.generateOutlines(connectedFaces);
if (outlines.length > 1) {
console.log(`Surface with ${connectedFaces.length} faces has ${outlines.length} outlines:`);
outlines.forEach((outline, idx) => {
console.log(` Outline ${idx}: ${outline.length} vertices`);
});
}
// Sort outlines by area (largest first = outer boundary)
// Use shoelace formula to calculate signed area
const outlinesWithArea = outlines.map(outline => {
let area = 0;
for (let i = 0; i < outline.length; i++) {
const p1 = outline[i];
const p2 = outline[(i + 1) % outline.length];
area += (p1.x * p2.y - p2.x * p1.y);
}
return { outline, area: Math.abs(area) };
});
// Sort by area descending (largest = outer boundary)
outlinesWithArea.sort((a, b) => b.area - a.area);
const sortedOutlines = outlinesWithArea.map(o => o.outline);
surfaces.push({ faces: connectedFaces, outlines: sortedOutlines });
}
}
console.log(`Merged ${totalFaces} triangles into ${surfaces.length} faces`);
// Generate STEP file
let step = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('STL to STEP Conversion with Face Merging'),'2;1');
FILE_NAME('${productName}.step','${new Date().toISOString()}',(''),(''),'stl-to-step','','');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));
ENDSEC;
DATA;
`;
let id = 1;
// Units mapping
const unitMultiplier = {
'mm': '.MILLI.',
'cm': '.CENTI.',
'm': '',
'inch': '.INCH.'
};
const unitPrefix = unitMultiplier[units] || '.MILLI.';
// Units and context
const lengthUnit = id++;
const planeAngleUnit = id++;
const solidAngleUnit = id++;
step += `#${lengthUnit}=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(${unitPrefix},.METRE.));\n`;
step += `#${planeAngleUnit}=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));\n`;
step += `#${solidAngleUnit}=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());\n`;
const uncertainty = id++;
step += `#${uncertainty}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#${lengthUnit},'distance_accuracy_value','confusion accuracy');\n`;
// Geometric representation context
const geomContextId = id++;
step += `#${geomContextId}=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#${uncertainty}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#${lengthUnit},#${planeAngleUnit},#${solidAngleUnit}))REPRESENTATION_CONTEXT('ID1','3D'));\n`;
// Application context
const appContext = id++;
step += `#${appContext}=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');\n`;
const appProtocol = id++;
step += `#${appProtocol}=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',1998,#${appContext});\n`;
// Product definition
const productId = id++;
step += `#${productId}=PRODUCT('${productName}','${productName}','',(#${appContext}));\n`;
const productDefFormId = id++;
step += `#${productDefFormId}=PRODUCT_DEFINITION_FORMATION('','',#${productId});\n`;
const productDefCtx = id++;
step += `#${productDefCtx}=PRODUCT_DEFINITION_CONTEXT('part definition',#${appContext},'design');\n`;
const productDefId = id++;
step += `#${productDefId}=PRODUCT_DEFINITION('design','',#${productDefFormId},#${productDefCtx});\n`;
// Shape representation - reserve ID
const shapeRepId = id++;
// Build vertex map and IDs
const vertexMap = new Map();
const cartesianPointIds = new Map();
const vertexPointIds = new Map();
function getOrCreateVertex(x, y, z) {
const key = `${x.toFixed(6)},${y.toFixed(6)},${z.toFixed(6)}`;
if (!vertexMap.has(key)) {
// Create cartesian point
const pointId = id++;
step += `#${pointId}=CARTESIAN_POINT('',(${x.toFixed(6)},${y.toFixed(6)},${z.toFixed(6)}));\n`;
cartesianPointIds.set(key, pointId);
// Create vertex point
const vpId = id++;
step += `#${vpId}=VERTEX_POINT('',#${pointId});\n`;
vertexPointIds.set(key, vpId);
vertexMap.set(key, { pointId, vpId, x, y, z });
}
return vertexMap.get(key);
}
// Advanced B-rep shape representation
const manifoldId = id++;
const closedShellId = id++;
step += `#${shapeRepId}=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#${manifoldId}),#${geomContextId});\n`;
step += `#${manifoldId}=MANIFOLD_SOLID_BREP('',#${closedShellId});\n`;
// Reserve face IDs
const faceIds = [];
for (let i = 0; i < surfaces.length; i++) {
faceIds.push(id++);
}
step += `#${closedShellId}=CLOSED_SHELL('',(${faceIds.map(fid => `#${fid}`).join(',')}));\n`;
// Helper function to merge co-linear edges in an outline
function mergeColinearEdges(outline) {
if (outline.length < 3) return outline;
const tolerance = 1e-6;
// Helper to check if three points are co-linear
function areColinear(p1, p2, p3) {
const v1 = new Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
const v2 = new Vector3(p3.x - p2.x, p3.y - p2.y, p3.z - p2.z);
const len1 = v1.length();
const len2 = v2.length();
if (len1 < tolerance || len2 < tolerance) return true;
v1.normalize();
v2.normalize();
// Check if vectors are parallel (dot product close to 1 or -1)
const dot = Math.abs(v1.dot(v2));
return dot > 1 - tolerance;
}
// Mark vertices to keep
const keep = new Array(outline.length).fill(true);
// Check each vertex to see if it's on a straight line
for (let i = 0; i < outline.length; i++) {
const prev = outline[(i - 1 + outline.length) % outline.length];
const curr = outline[i];
const next = outline[(i + 1) % outline.length];
if (areColinear(prev, curr, next)) {
keep[i] = false;
}
}
// Build merged array with only kept vertices
const merged = [];
for (let i = 0; i < outline.length; i++) {
if (keep[i]) {
merged.push(outline[i]);
}
}
return merged.length >= 3 ? merged : outline;
}
// Create each merged face
for (let surfaceIdx = 0; surfaceIdx < surfaces.length; surfaceIdx++) {
const surface = surfaces[surfaceIdx];
const faceId = faceIds[surfaceIdx];
if (!surface.outlines || surface.outlines.length === 0) {
console.warn(`Skipping invalid surface ${surfaceIdx} with no outlines`);
continue;
}
// Process all outlines: first is outer boundary, rest are holes
const boundIds = [];
let allVertices = [];
for (let outlineIdx = 0; outlineIdx < surface.outlines.length; outlineIdx++) {
let outline = surface.outlines[outlineIdx];
if (!outline || outline.length < 3) {
console.warn(`Skipping invalid outline ${outlineIdx} in surface ${surfaceIdx}`);
continue;
}
// Merge co-linear edges
const originalLength = outline.length;
outline = mergeColinearEdges(outline);
if (originalLength !== outline.length) {
console.log(` Merged outline ${outlineIdx}: ${originalLength} -> ${outline.length} vertices`);
}
if (outline.length < 3) {
console.warn(`Outline ${outlineIdx} in surface ${surfaceIdx} has < 3 vertices after merging`);
continue;
}
const boundId = id++;
const edgeLoopId = id++;
// Create edges for this outline
const orientedEdgeIds = [];
const vertices = [];
// Get vertices and create vertex records
for (let i = 0; i < outline.length; i++) {
const pt = outline[i];
const vertex = getOrCreateVertex(pt.x, pt.y, pt.z);
vertices.push(vertex);
}
// Store vertices from first outline for normal calculation
if (outlineIdx === 0) {
allVertices = vertices;
}
// For holes, reverse the vertex order to get opposite winding
const orderedVertices = outlineIdx === 0 ? vertices : vertices.slice().reverse();
// Create edges
for (let i = 0; i < orderedVertices.length; i++) {
const v1 = orderedVertices[i];
const v2 = orderedVertices[(i + 1) % orderedVertices.length];
const dx = v2.x - v1.x;
const dy = v2.y - v1.y;
const dz = v2.z - v1.z;
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-10) {
console.warn(`Warning: Degenerate edge in surface ${surfaceIdx}, outline ${outlineIdx}`);
continue;
}
const orientedEdgeId = id++;
const edgeCurveId = id++;
const lineId = id++;
const vectorId = id++;
const directionId = id++;
orientedEdgeIds.push(orientedEdgeId);
step += `#${directionId}=DIRECTION('',(${(dx / len).toFixed(6)},${(dy / len).toFixed(6)},${(dz / len).toFixed(6)}));\n`;
step += `#${vectorId}=VECTOR('',#${directionId},${len.toFixed(6)});\n`;
step += `#${lineId}=LINE('',#${v1.pointId},#${vectorId});\n`;
step += `#${edgeCurveId}=EDGE_CURVE('',#${v1.vpId},#${v2.vpId},#${lineId},.T.);\n`;
step += `#${orientedEdgeId}=ORIENTED_EDGE('',*,*,#${edgeCurveId},.T.);\n`;
}
if (orientedEdgeIds.length === 0) {
console.warn(`No valid edges in outline ${outlineIdx} of surface ${surfaceIdx}`);
continue;
}
step += `#${edgeLoopId}=EDGE_LOOP('',(${orientedEdgeIds.map(eid => `#${eid}`).join(',')}));\n`;
// First outline is FACE_OUTER_BOUND, subsequent ones are holes (FACE_BOUND)
if (outlineIdx === 0) {
step += `#${boundId}=FACE_OUTER_BOUND('',#${edgeLoopId},.T.);\n`;
} else {
step += `#${boundId}=FACE_BOUND('',#${edgeLoopId},.T.);\n`;
}
boundIds.push(boundId);
}
if (boundIds.length === 0 || allVertices.length < 3) {
console.warn(`Skipping surface ${surfaceIdx} - no valid bounds`);
continue;
}
// Calculate face normal from first three vertices of outer boundary
const v0 = new Vector3(allVertices[0].x, allVertices[0].y, allVertices[0].z);
const v1 = new Vector3(allVertices[1].x, allVertices[1].y, allVertices[1].z);
const v2 = new Vector3(allVertices[2].x, allVertices[2].y, allVertices[2].z);
const edge1 = v1.clone().sub(v0);
const edge2 = v2.clone().sub(v0);
const normal = edge1.clone().cross(edge2).normalize();
// Calculate face center for plane origin
let centerX = 0, centerY = 0, centerZ = 0;
for (let vertex of allVertices) {
centerX += vertex.x;
centerY += vertex.y;
centerZ += vertex.z;
}
centerX /= allVertices.length;
centerY /= allVertices.length;
centerZ /= allVertices.length;
const centerPointId = id++;
step += `#${centerPointId}=CARTESIAN_POINT('',(${centerX.toFixed(6)},${centerY.toFixed(6)},${centerZ.toFixed(6)}));\n`;
const planeId = id++;
const axisPlacementId = id++;
const normalDirId = id++;
const refDirId = id++;
step += `#${normalDirId}=DIRECTION('',(${normal.x.toFixed(6)},${normal.y.toFixed(6)},${normal.z.toFixed(6)}));\n`;
const refDir = edge1.normalize();
step += `#${refDirId}=DIRECTION('',(${refDir.x.toFixed(6)},${refDir.y.toFixed(6)},${refDir.z.toFixed(6)}));\n`;
step += `#${axisPlacementId}=AXIS2_PLACEMENT_3D('',#${centerPointId},#${normalDirId},#${refDirId});\n`;
step += `#${planeId}=PLANE('',#${axisPlacementId});\n`;
// Create ADVANCED_FACE with all bounds (outer + holes)
step += `#${faceId}=ADVANCED_FACE('',(${boundIds.map(bid => `#${bid}`).join(',')}),#${planeId},.T.);\n`;
}
// Product definition shape
const prodDefShapeId = id++;
step += `#${prodDefShapeId}=PRODUCT_DEFINITION_SHAPE('','',#${productDefId});\n`;
step += `#${id++}=SHAPE_DEFINITION_REPRESENTATION(#${prodDefShapeId},#${shapeRepId});\n`;
step += `ENDSEC;\nEND-ISO-10303-21;\n`;
return step;
}