523 lines
18 KiB
C++
523 lines
18 KiB
C++
//-----------------------------------------------------------------------------
|
|
// Intermediate Data Format (IDF) file reader. Reads an IDF file for PCB outlines and creates
|
|
// an equivalent SovleSpace sketch/extrusion. Supports only Linking, not import.
|
|
// Part placement is not currently supported.
|
|
//
|
|
// Copyright 2020 Paul Kahler.
|
|
//-----------------------------------------------------------------------------
|
|
#include "solvespace.h"
|
|
#include "sketch.h"
|
|
|
|
// Split a string into substrings separated by spaces.
|
|
// Allow quotes to enclose spaces within a string
|
|
static std::vector <std::string> splitString(const std::string line) {
|
|
std::vector <std::string> v = {};
|
|
|
|
if(line.length() == 0) return v;
|
|
|
|
std::string s = "";
|
|
bool inString = false;
|
|
bool inQuotes = false;
|
|
|
|
for (size_t i=0; i<line.length(); i++) {
|
|
char c = line.at(i);
|
|
if (inQuotes) {
|
|
if (c != '"') {
|
|
s.push_back(c);
|
|
} else {
|
|
v.push_back(s);
|
|
inQuotes = false;
|
|
inString = false;
|
|
s = "";
|
|
}
|
|
} else if (inString) {
|
|
if (c != ' ') {
|
|
s.push_back(c);
|
|
} else {
|
|
v.push_back(s);
|
|
inString = false;
|
|
s = "";
|
|
}
|
|
} else if(c == '"') {
|
|
inString = true;
|
|
inQuotes = true;
|
|
} else if(c != ' ') {
|
|
s = "";
|
|
s.push_back(c);
|
|
inString = true;
|
|
}
|
|
}
|
|
if(s.length() > 0)
|
|
v.push_back(s);
|
|
|
|
return v;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Functions for linking an IDF file - we need to create entites that
|
|
// get remapped into a linked group similar to linking .slvs files
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Make a new point - type doesn't matter since we will make a copy later
|
|
static hEntity newPoint(EntityList *el, int *id, Vector p, bool visible = true) {
|
|
Entity en = {};
|
|
en.type = Entity::Type::POINT_N_COPY;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 462;
|
|
en.actPoint = p;
|
|
en.construction = false;
|
|
en.style.v = Style::DATUM;
|
|
en.actVisible = visible;
|
|
en.forceHidden = false;
|
|
|
|
*id = *id+1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static hEntity newLine(EntityList *el, int *id, hEntity p0, hEntity p1, bool keepout) {
|
|
Entity en = {};
|
|
en.type = Entity::Type::LINE_SEGMENT;
|
|
en.point[0] = p0;
|
|
en.point[1] = p1;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 493;
|
|
en.construction = keepout;
|
|
en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP;
|
|
en.actVisible = true;
|
|
en.forceHidden = false;
|
|
|
|
*id = *id+1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static hEntity newNormal(EntityList *el, int *id, Quaternion normal) {
|
|
// normals have parameters, but we don't need them to make a NORMAL_N_COPY from this
|
|
Entity en = {};
|
|
en.type = Entity::Type::NORMAL_N_COPY;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 472;
|
|
en.actNormal = normal;
|
|
en.construction = false;
|
|
en.style.v = Style::ACTIVE_GRP;
|
|
// to be visible we need to add a point.
|
|
en.point[0] = newPoint(el, id, Vector::From(0,0,3), /*visible=*/ true);
|
|
en.actVisible = true;
|
|
en.forceHidden = false;
|
|
|
|
*id = *id+1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static hEntity newArc(EntityList *el, int *id, hEntity p0, hEntity p1, hEntity pc, hEntity hnorm, bool keepout) {
|
|
Entity en = {};
|
|
en.type = Entity::Type::ARC_OF_CIRCLE;
|
|
en.point[0] = pc;
|
|
en.point[1] = p0;
|
|
en.point[2] = p1;
|
|
en.normal = hnorm;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 403;
|
|
en.construction = keepout;
|
|
en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP;
|
|
en.actVisible = true;
|
|
en.forceHidden = false; *id = *id+1;
|
|
|
|
*id = *id + 1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static hEntity newDistance(EntityList *el, int *id, double distance) {
|
|
// normals have parameters, but we don't need them to make a NORMAL_N_COPY from this
|
|
Entity en = {};
|
|
en.type = Entity::Type::DISTANCE;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 472;
|
|
en.actDistance = distance;
|
|
en.construction = false;
|
|
en.style.v = Style::ACTIVE_GRP;
|
|
// to be visible we'll need to add a point?
|
|
en.actVisible = false;
|
|
en.forceHidden = false;
|
|
|
|
*id = *id+1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static hEntity newCircle(EntityList *el, int *id, hEntity p0, hEntity hdist, hEntity hnorm, bool keepout) {
|
|
Entity en = {};
|
|
en.type = Entity::Type::CIRCLE;
|
|
en.point[0] = p0;
|
|
en.normal = hnorm;
|
|
en.distance = hdist;
|
|
en.extraPoints = 0;
|
|
en.timesApplied = 0;
|
|
en.group.v = 399;
|
|
en.construction = keepout;
|
|
en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP;
|
|
en.actVisible = true;
|
|
en.forceHidden = false;
|
|
|
|
*id = *id+1;
|
|
en.h.v = *id + en.group.v*65536;
|
|
el->Add(&en);
|
|
return en.h;
|
|
}
|
|
|
|
static Vector ArcCenter(Vector p0, Vector p1, double angle) {
|
|
// locate the center of an arc
|
|
Vector m = p0.Plus(p1).ScaledBy(0.5);
|
|
Vector perp = Vector::From(p1.y-p0.y, p0.x-p1.x, 0.0).WithMagnitude(1.0);
|
|
double dist = 0;
|
|
if (angle != 180) {
|
|
dist = (p1.Minus(m).Magnitude())/tan(0.5*angle*3.141592653589793/180.0);
|
|
} else {
|
|
dist = 0.0;
|
|
}
|
|
Vector c = m.Minus(perp.ScaledBy(dist));
|
|
return c;
|
|
}
|
|
|
|
// Add an IDF line or arc to the entity list. According to spec, zero angle indicates a line.
|
|
// Positive angles are counter clockwise, negative are clockwise. An angle of 360
|
|
// indicates a circle centered at x1,y1 passing through x2,y2 and is a complete loop.
|
|
static void CreateEntity(EntityList *el, int *id, hEntity h0, hEntity h1, hEntity hnorm,
|
|
Vector p0, Vector p1, double angle, bool keepout) {
|
|
if (angle == 0.0) {
|
|
//line
|
|
if(p0.Equals(p1)) return;
|
|
|
|
newLine(el, id, h0, h1, keepout);
|
|
|
|
} else if(angle == 360.0) {
|
|
// circle
|
|
double d = p1.Minus(p0).Magnitude();
|
|
hEntity hd = newDistance(el, id, d);
|
|
newCircle(el, id, h1, hd, hnorm, keepout);
|
|
|
|
} else {
|
|
// arc
|
|
if(angle < 0.0) {
|
|
swap(p0,p1);
|
|
swap(h0,h1);
|
|
}
|
|
// locate the center of the arc
|
|
Vector m = p0.Plus(p1).ScaledBy(0.5);
|
|
Vector perp = Vector::From(p1.y-p0.y, p0.x-p1.x, 0.0).WithMagnitude(1.0);
|
|
double dist = 0;
|
|
if (angle != 180) {
|
|
dist = (p1.Minus(m).Magnitude())/tan(0.5*angle*3.141592653589793/180.0);
|
|
} else {
|
|
dist = 0.0;
|
|
}
|
|
Vector c = m.Minus(perp.ScaledBy(dist));
|
|
hEntity hc = newPoint(el, id, c, /*visible=*/false);
|
|
newArc(el, id, h0, h1, hc, hnorm, keepout);
|
|
}
|
|
}
|
|
|
|
// borrowed from Entity::GenerateBezierCurves because we don't have parameters.
|
|
static void MakeBeziersForArcs(SBezierList *sbl, Vector center, Vector pa, Vector pb,
|
|
Quaternion q, double angle) {
|
|
|
|
Vector u = q.RotationU(), v = q.RotationV();
|
|
double r = pa.Minus(center).Magnitude();
|
|
double theta, dtheta;
|
|
|
|
if(angle == 360.0) {
|
|
theta = 0;
|
|
} else {
|
|
Point2d c2 = center.Project2d(u, v);
|
|
Point2d pa2 = (pa.Project2d(u, v)).Minus(c2);
|
|
|
|
theta = atan2(pa2.y, pa2.x);
|
|
}
|
|
dtheta = angle * PI/180;
|
|
|
|
int i, n;
|
|
if(dtheta > (3*PI/2 + 0.01)) {
|
|
n = 4;
|
|
} else if(dtheta > (PI + 0.01)) {
|
|
n = 3;
|
|
} else if(dtheta > (PI/2 + 0.01)) {
|
|
n = 2;
|
|
} else {
|
|
n = 1;
|
|
}
|
|
dtheta /= n;
|
|
|
|
for(i = 0; i < n; i++) {
|
|
double s, c;
|
|
|
|
c = cos(theta);
|
|
s = sin(theta);
|
|
// The start point of the curve, and the tangent vector at
|
|
// that start point.
|
|
Vector p0 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)),
|
|
t0 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c));
|
|
|
|
theta += dtheta;
|
|
|
|
c = cos(theta);
|
|
s = sin(theta);
|
|
Vector p2 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)),
|
|
t2 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c));
|
|
|
|
// The control point must lie on both tangents.
|
|
Vector p1 = Vector::AtIntersectionOfLines(p0, p0.Plus(t0),
|
|
p2, p2.Plus(t2),
|
|
NULL);
|
|
|
|
SBezier sb = SBezier::From(p0, p1, p2);
|
|
sb.weight[1] = cos(dtheta/2);
|
|
sbl->l.Add(&sb);
|
|
}
|
|
}
|
|
|
|
namespace SolveSpace {
|
|
|
|
// Here we read the important section of an IDF file. SolveSpace Entities are directly created by
|
|
// the funcions above, which is only OK because of the way linking works. For example points do
|
|
// not have handles for solver parameters (coordinates), they only have their actPoint values
|
|
// set (or actNormal or actDistance). These are incompete entites and would be a problem if
|
|
// they were part of the sketch, but they are not. After making a list of them here, a new group
|
|
// gets created from copies of these. Those copies are complete and part of the sketch group.
|
|
bool LinkIDF(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *sh) {
|
|
dbp("\nLink IDF board outline.");
|
|
el->Clear();
|
|
std::string data;
|
|
if(!ReadFile(filename, &data)) {
|
|
Error("Couldn't read from '%s'", filename.raw.c_str());
|
|
return false;
|
|
}
|
|
|
|
enum IDF_SECTION {
|
|
none,
|
|
header,
|
|
board_outline,
|
|
other_outline,
|
|
routing_outline,
|
|
placement_outline,
|
|
routing_keepout,
|
|
via_keepout,
|
|
placement_group,
|
|
drilled_holes,
|
|
notes,
|
|
component_placement
|
|
} section;
|
|
|
|
section = IDF_SECTION::none;
|
|
int record_number = 0;
|
|
int curve = -1;
|
|
int entityCount = 0;
|
|
|
|
hEntity hprev;
|
|
hEntity hprevTop;
|
|
Vector pprev = Vector::From(0,0,0);
|
|
Vector pprevTop = Vector::From(0,0,0);
|
|
|
|
double board_thickness = 10.0;
|
|
double scale = 1.0; //mm
|
|
bool topEntities, bottomEntities;
|
|
|
|
Quaternion normal = Quaternion::From(Vector::From(1,0,0), Vector::From(0,1,0));
|
|
hEntity hnorm = newNormal(el, &entityCount, normal);
|
|
|
|
// to create the extursion we will need to collect a set of bezier curves defined
|
|
// by the perimeter, cutouts, and holes.
|
|
SBezierList sbl = {};
|
|
|
|
std::stringstream stream(data);
|
|
for(std::string line; getline( stream, line ); ) {
|
|
if (line.find(".END_") == 0) {
|
|
section = none;
|
|
curve = -1;
|
|
}
|
|
switch (section) {
|
|
case none:
|
|
if(line.find(".HEADER") == 0) {
|
|
section = header;
|
|
record_number = 1;
|
|
} else if (line.find(".BOARD_OUTLINE") == 0) {
|
|
section = board_outline;
|
|
record_number = 1;
|
|
} else if (line.find(".ROUTE_KEEPOUT") == 0) {
|
|
section = routing_keepout;
|
|
record_number = 1;
|
|
} else if(line.find(".DRILLED_HOLES") == 0) {
|
|
section = drilled_holes;
|
|
record_number = 1;
|
|
}
|
|
break;
|
|
|
|
case header:
|
|
if(record_number == 3) {
|
|
if(line.find("MM") != std::string::npos) {
|
|
dbp("IDF units are MM");
|
|
scale = 1.0;
|
|
} else if(line.find("THOU") != std::string::npos) {
|
|
dbp("IDF units are thousandths of an inch");
|
|
scale = 0.0254;
|
|
} else {
|
|
dbp("IDF import, no units found in file.");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case routing_keepout:
|
|
case board_outline:
|
|
if (record_number == 2) {
|
|
if(section == board_outline) {
|
|
topEntities = true;
|
|
bottomEntities = true;
|
|
board_thickness = std::stod(line) * scale;
|
|
dbp("IDF board thickness: %lf", board_thickness);
|
|
} else if (section == routing_keepout) {
|
|
topEntities = false;
|
|
bottomEntities = false;
|
|
if(line.find("TOP") == 0 || line.find("BOTH") == 0)
|
|
topEntities = true;
|
|
if(line.find("BOTTOM") == 0 || line.find("BOTH") == 0)
|
|
bottomEntities = true;
|
|
}
|
|
} else { // records 3+ are lines, arcs, and circles
|
|
std::vector <std::string> values = splitString(line);
|
|
if(values.size() != 4) continue;
|
|
int c = stoi(values[0]);
|
|
double x = stof(values[1]);
|
|
double y = stof(values[2]);
|
|
double ang = stof(values[3]);
|
|
Vector point = Vector::From(x,y,0.0);
|
|
Vector pTop = Vector::From(x,y,board_thickness);
|
|
if(c != curve) { // start a new curve
|
|
curve = c;
|
|
if (bottomEntities)
|
|
hprev = newPoint(el, &entityCount, point, /*visible=*/false);
|
|
if (topEntities)
|
|
hprevTop = newPoint(el, &entityCount, pTop, /*visible=*/false);
|
|
pprev = point;
|
|
pprevTop = pTop;
|
|
} else {
|
|
if(section == board_outline) {
|
|
// create a bezier for the extrusion
|
|
if (ang == 0) {
|
|
// straight lines
|
|
SBezier sb = SBezier::From(pprev, point);
|
|
sbl.l.Add(&sb);
|
|
} else if (ang != 360.0) {
|
|
// Arcs
|
|
Vector c = ArcCenter(pprev, point, ang);
|
|
MakeBeziersForArcs(&sbl, c, pprev, point, normal, ang);
|
|
} else {
|
|
// circles
|
|
MakeBeziersForArcs(&sbl, point, pprev, pprev, normal, ang);
|
|
}
|
|
}
|
|
// next create the entities
|
|
// only curves and points at circle centers will be visible
|
|
bool vis = (ang == 360.0);
|
|
if (bottomEntities) {
|
|
hEntity hp = newPoint(el, &entityCount, point, /*visible=*/vis);
|
|
CreateEntity(el, &entityCount, hprev, hp, hnorm, pprev, point, ang,
|
|
(section == routing_keepout) );
|
|
pprev = point;
|
|
hprev = hp;
|
|
}
|
|
if (topEntities) {
|
|
hEntity hp = newPoint(el, &entityCount, pTop, /*visible=*/vis);
|
|
CreateEntity(el, &entityCount, hprevTop, hp, hnorm, pprevTop, pTop,
|
|
ang, (section == routing_keepout) );
|
|
pprevTop = pTop;
|
|
hprevTop = hp;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case other_outline:
|
|
case routing_outline:
|
|
case placement_outline:
|
|
case via_keepout:
|
|
case placement_group:
|
|
break;
|
|
|
|
case drilled_holes: {
|
|
std::vector <std::string> values = splitString(line);
|
|
if(values.size() < 6) continue;
|
|
double d = stof(values[0]);
|
|
double x = stof(values[1]);
|
|
double y = stof(values[2]);
|
|
// Only show holes likely to be useful in MCAD to reduce complexity.
|
|
if((d > 1.7) || (values[5].compare(0,3,"PIN") == 0)
|
|
|| (values[5].compare(0,3,"MTG") == 0)) {
|
|
// create the entity
|
|
Vector cent = Vector::From(x,y,0.0);
|
|
hEntity hcent = newPoint(el, &entityCount, cent);
|
|
hEntity hdist = newDistance(el, &entityCount, d/2);
|
|
newCircle(el, &entityCount, hcent, hdist, hnorm, false);
|
|
// and again for the top
|
|
Vector cTop = Vector::From(x,y,board_thickness);
|
|
hcent = newPoint(el, &entityCount, cTop);
|
|
hdist = newDistance(el, &entityCount, d/2);
|
|
newCircle(el, &entityCount, hcent, hdist, hnorm, false);
|
|
// create the curves for the extrusion
|
|
Vector pt = Vector::From(x+d/2, y, 0.0);
|
|
MakeBeziersForArcs(&sbl, cent, pt, pt, normal, 360.0);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case notes:
|
|
case component_placement:
|
|
break;
|
|
|
|
default:
|
|
section = none;
|
|
break;
|
|
}
|
|
record_number++;
|
|
}
|
|
// now we can create an extrusion from all the Bezier curves. We can skip things
|
|
// like checking for a coplanar sketch because everything is at z=0.
|
|
SPolygon polyLoops = {};
|
|
bool allClosed;
|
|
bool allCoplanar;
|
|
Vector errorPointAt = Vector::From(0,0,0);
|
|
SEdge errorAt = {};
|
|
|
|
SBezierLoopSetSet sblss = {};
|
|
sblss.FindOuterFacesFrom(&sbl, &polyLoops, NULL,
|
|
100.0, &allClosed, &errorAt,
|
|
&allCoplanar, &errorPointAt, NULL);
|
|
|
|
//hack for when there is no sketch yet and the first group is a linked IDF
|
|
double ctc = SS.chordTolCalculated;
|
|
if(ctc == 0.0) SS.chordTolCalculated = 0.1; //mm
|
|
// there should only by one sbls in the sblss unless a board has disjointed parts...
|
|
sh->MakeFromExtrusionOf(sblss.l.First(), Vector::From(0.0, 0.0, 0.0),
|
|
Vector::From(0.0, 0.0, board_thickness),
|
|
RgbaColor::From(0, 180, 0) );
|
|
SS.chordTolCalculated = ctc;
|
|
sblss.Clear();
|
|
sbl.Clear();
|
|
sh->booleanFailed = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|