227 lines
7.1 KiB
JavaScript
227 lines
7.1 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
import { polygons } from '../geo/polygons.js';
|
|
import { CSG } from '../geo/csg.js';
|
|
import * as TracespaceParser from '../ext/gerber.js';
|
|
import { newPolygon } from '../geo/polygon.js';
|
|
import { newBounds } from '../geo/bounds.js';
|
|
|
|
export function toMesh(text, opt = {}) {
|
|
const progress = opt.progress || function(){};
|
|
const debug = opt.debug ? console.log : function(){};
|
|
|
|
// for now ignore open trace lines
|
|
// future exports should allow a way to represent them for cnc
|
|
const { open, closed, circs, rects } = parse(text);
|
|
progress(5);
|
|
|
|
// for the copper layer:
|
|
// should produce one top level poly which is the board outline
|
|
// next level down should be the remaining copper trace which we
|
|
// subtract from the board to produce paths that can be milled
|
|
const nest = polygons.nest([...closed, ...circs, ...rects], true);
|
|
debug({ nest });
|
|
|
|
if (nest.length === 0) return [].toFloat32();
|
|
|
|
const flat = [];
|
|
for (let n of nest) n.flattenTo(flat, [], true);
|
|
progress(8);
|
|
debug({ flat });
|
|
|
|
const z0 = opt.z || opt.z0 || 1;
|
|
const z1 = opt.z1 || 0.25;
|
|
|
|
// encapsulate all polys in one enclosing rect with an offset
|
|
if (nest.length > 1) {
|
|
const offset = (opt.offset || 2) * 2;
|
|
const bounds = nest[0].bounds;
|
|
for (let p of nest) {
|
|
bounds.merge(p.bounds);
|
|
}
|
|
const rect = newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset);
|
|
for (let n of nest) {
|
|
n.depth++;
|
|
}
|
|
flat.push(rect);
|
|
debug({ bounds, flat });
|
|
opt.raise = z1;
|
|
}
|
|
|
|
// board outline
|
|
const top = [];
|
|
for (let poly of flat.filter(p => p.depth === 0)) {
|
|
top.push(poly.extrude(z0));
|
|
}
|
|
progress(15);
|
|
debug({ top });
|
|
|
|
// nothing to render
|
|
if (top.length === 0) {
|
|
return [].toFloat32();
|
|
}
|
|
|
|
// copper pads
|
|
const mid = [];
|
|
for (let poly of flat.filter(p => p.depth === 1)) {
|
|
mid.push(poly.extrude(z1, { zadd: z0 - z1 }));
|
|
}
|
|
progress(20);
|
|
debug({ mid });
|
|
|
|
// if no traces, return board
|
|
if (mid.length === 0) {
|
|
return top[0];
|
|
}
|
|
|
|
// copper traces (expand first since open)
|
|
const open_exp = open.map(poly => {
|
|
const diam = poly.tool?.shape?.diameter;
|
|
return diam ? poly.offset_open(diam / 2, 'round') : null;
|
|
}).filter(p => p).flat();
|
|
|
|
// union open trace expansions
|
|
const opn = [];
|
|
for (let poly of polygons.union(open_exp, 0, true)) {
|
|
const tool = poly.tool;
|
|
opn.push(poly.extrude(z0 + (opt.raise || 0)));
|
|
}
|
|
progress(25);
|
|
debug({ opn });
|
|
|
|
// inner polys for traces which we add back to the top
|
|
// and (optionally) remove from the bottom for making vias
|
|
const low = [];
|
|
for (let poly of flat.filter(p => p.depth === 2)) {
|
|
low.push(poly.extrude(z1, { zadd: z0 - z1 }));
|
|
}
|
|
progress(30);
|
|
|
|
// convert vertex arrays to csg mesh definitions
|
|
const topMesh = CSG.fromPositionArray(top[0]);
|
|
const midMesh = mid.map(v => CSG.fromPositionArray(v));
|
|
progress(35);
|
|
|
|
// subtract mid from top
|
|
let meshOut = CSG.subtract(topMesh, ...midMesh);
|
|
progress(50);
|
|
|
|
if (opn.length) {
|
|
// add back raised traces (before removing vias)
|
|
const opnMesh = opn.map(v => CSG.fromPositionArray(v))
|
|
meshOut = CSG.union(meshOut, ...opnMesh);
|
|
}
|
|
progress(70);
|
|
|
|
if (low.length) {
|
|
// subtract low (vias) from remaining
|
|
const lowMesh = low.map(v => CSG.fromPositionArray(v))
|
|
meshOut = CSG.union(meshOut, ...lowMesh);
|
|
}
|
|
progress(90);
|
|
|
|
return CSG.toPositionArray(meshOut);
|
|
}
|
|
|
|
export function parse(text) {
|
|
let fact = 1000000;
|
|
let tools = {};
|
|
let tool;
|
|
let poly;
|
|
let polys = [];
|
|
let circs = [];
|
|
let rects = [];
|
|
let last = {};
|
|
let bounds = newBounds();
|
|
let p = TracespaceParser.createParser();
|
|
p.feed(text);
|
|
let r = p.results();
|
|
let c = r.children
|
|
.filter(c => {
|
|
switch (c.type) {
|
|
case 'toolDefinition':
|
|
tools[c.code] = c.shape;
|
|
break;
|
|
case 'toolChange':
|
|
tool = { code: c.code, shape: tools[c.code] };
|
|
break;
|
|
case 'graphic':
|
|
c.tool = tool;
|
|
return true;
|
|
}
|
|
return false;
|
|
})
|
|
.map(r => {
|
|
return {
|
|
type: r.graphic,
|
|
tool: r.tool,
|
|
x: parseInt(r.coordinates.x),
|
|
y: parseInt(r.coordinates.y)
|
|
};
|
|
})
|
|
.map(r => {
|
|
const { x, y, tool, type } = r;
|
|
const pos = { x:x/fact, y:y/fact, z: 0};
|
|
bounds.update(pos);
|
|
switch (type) {
|
|
case 'move':
|
|
if (true && poly && last.x === x && last.y === y && last.tool === tool) {
|
|
// console.log('continuation');
|
|
} else {
|
|
poly = newPolygon().setOpen().add(pos.x, pos.y, 0);
|
|
poly.tool = tool;
|
|
polys.push(poly);
|
|
}
|
|
break;
|
|
case 'segment':
|
|
poly.add(pos.x, pos.y, 0);
|
|
break;
|
|
case 'shape':
|
|
poly = undefined;
|
|
let { shape } = tool;
|
|
switch (shape.type) {
|
|
case 'obround':
|
|
if (shape.xSize === shape.ySize) {
|
|
circs.push(newPolygon().centerCircle(pos, shape.xSize / 2, 20));
|
|
} else {
|
|
console.log('TODO', shape);
|
|
}
|
|
break;
|
|
case 'circle':
|
|
circs.push(newPolygon().centerCircle(pos, shape.diameter / 2, 20));
|
|
break;
|
|
case 'rectangle':
|
|
rects.push(newPolygon().centerRectangle(pos, shape.xSize, shape.ySize));
|
|
break;
|
|
default:
|
|
console.log('TODO', shape);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
last = { x, y, tool };
|
|
return r;
|
|
});
|
|
// center board on origin
|
|
const { minx, maxx, miny, maxy } = bounds;
|
|
for (let poly of [...polys, ...circs, ...rects]) {
|
|
poly.move({
|
|
x: -((maxx-minx)/2 + minx),
|
|
y: -((maxy-miny)/2 + miny),
|
|
z: 0
|
|
});
|
|
}
|
|
// separate open and closed polys (traces vs areas)
|
|
const open = [];
|
|
const closed = [];
|
|
for (let poly of polys) {
|
|
if (poly.appearsClosed()) {
|
|
// clean up gnarly overlapping lines
|
|
// make contiguous, simpler enclosed areas
|
|
closed.push(...poly.simplify());
|
|
} else {
|
|
open.push(poly);
|
|
}
|
|
}
|
|
return { open, closed, circs, rects };
|
|
}
|