//----------------------------------------------------------------------------- // 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 splitString(const std::string line) { std::vector v = {}; if(line.length() == 0) return v; std::string s = ""; bool inString = false; bool inQuotes = false; for (size_t i=0; i 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) { 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 = false; en.style.v = 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) { 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 = false; en.style.v = 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) { 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 = false; en.style.v = 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) { if (angle == 0.0) { //line if(p0.Equals(p1)) return; newLine(el, id, h0, h1); } else if(angle == 360.0) { // circle double d = p1.Minus(p0).Magnitude(); hEntity hd = newDistance(el, id, d); newCircle(el, id, h1, hd, hnorm); } 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); } } // 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; // no keepouts for now - they should also be shown as construction? // } 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 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); pprev = point; hprev = hp; } if (topEntities) { hEntity hp = newPoint(el, &entityCount, pTop, /*visible=*/vis); CreateEntity(el, &entityCount, hprevTop, hp, hnorm, pprevTop, pTop, ang); 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 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); // 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); // 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; } }