From 8cf9d68ecf6304fad9581c076e214b362bfa518a Mon Sep 17 00:00:00 2001 From: phkahler <14852918+phkahler@users.noreply.github.com> Date: Thu, 24 Sep 2020 12:30:06 -0400 Subject: [PATCH] IDF file Linking. Can read PCB outlines and cutouts, as well as Pin and Mounting holes. A simple PPCB model sans components is added to the assembly. --- src/CMakeLists.txt | 1 + src/file.cpp | 10 + src/group.cpp | 2 +- src/importidf.cpp | 500 +++++++++++++++++++++++++++++++++++++++++++ src/platform/gui.cpp | 5 + src/platform/gui.h | 2 + src/solvespace.h | 3 + 7 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 src/importidf.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbebccf..1b6d900 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -175,6 +175,7 @@ set(solvespace_core_SOURCES group.cpp groupmesh.cpp importdxf.cpp + importidf.cpp mesh.cpp modify.cpp mouse.cpp diff --git a/src/file.cpp b/src/file.cpp index 3cfcfbb..acb931f 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -702,6 +702,16 @@ void SolveSpaceUI::UpgradeLegacyData() { bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh) +{ + if(strcmp(filename.Extension().c_str(), "emn")==0) { + return LinkIDF(filename, le, m, sh); + } else { + return LoadEntitiesFromSlvs(filename, le, m, sh); + } +} + +bool SolveSpaceUI::LoadEntitiesFromSlvs(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh) { SSurface srf = {}; SCurve crv = {}; diff --git a/src/group.cpp b/src/group.cpp index 7a61616..1539e68 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -287,7 +287,7 @@ void Group::MenuGroup(Command id, Platform::Path linkFile) { g.meshCombine = CombineAs::ASSEMBLE; if(g.linkFile.IsEmpty()) { Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); - dialog->AddFilters(Platform::SolveSpaceModelFileFilters); + dialog->AddFilters(Platform::SolveSpaceLinkFileFilters); dialog->ThawChoices(settings, "LinkSketch"); if(!dialog->RunModal()) return; dialog->FreezeChoices(settings, "LinkSketch"); diff --git a/src/importidf.cpp b/src/importidf.cpp new file mode 100644 index 0000000..c1e2e33 --- /dev/null +++ b/src/importidf.cpp @@ -0,0 +1,500 @@ +//----------------------------------------------------------------------------- +// 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 thetaa, thetab, dtheta; + + if(angle == 360.0) { + thetaa = 0; + thetab = 2*PI; + dtheta = 2*PI; + } else { + Point2d c2 = center.Project2d(u, v); + Point2d pa2 = (pa.Project2d(u, v)).Minus(c2); + Point2d pb2 = (pb.Project2d(u, v)).Minus(c2); + + thetaa = atan2(pa2.y, pa2.x); + thetab = atan2(pb2.y, pb2.x); + dtheta = thetab - thetaa; + } + 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(thetaa); + s = sin(thetaa); + // 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)); + + thetaa += dtheta; + + c = cos(thetaa); + s = sin(thetaa); + 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 + + 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; + } + 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(".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 board_outline: + if (record_number == 2) { + board_thickness = std::stod(line) * scale; + dbp("IDF board thickness: %lf", board_thickness); + } 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; + hprev = newPoint(el, &entityCount, point, /*visible=*/false); + hprevTop = newPoint(el, &entityCount, pTop, /*visible=*/false); + pprev = point; + pprevTop = pTop; + } else { + // 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); + hEntity hp = newPoint(el, &entityCount, point, /*visible=*/vis); + CreateEntity(el, &entityCount, hprev, hp, hnorm, pprev, point, ang); + pprev = point; + hprev = hp; + 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 routing_keepout: + 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; +} + +} diff --git a/src/platform/gui.cpp b/src/platform/gui.cpp index b86d2ec..ff9b2cf 100644 --- a/src/platform/gui.cpp +++ b/src/platform/gui.cpp @@ -85,6 +85,11 @@ std::vector SolveSpaceModelFileFilters = { { CN_("file-type", "SolveSpace models"), { "slvs" } }, }; +std::vector SolveSpaceLinkFileFilters = { + { CN_("file-type", "SolveSpace models"), { "slvs" } }, + { CN_("file-type", "IDF circuit board"), { "emn" } }, +}; + std::vector RasterFileFilters = { { CN_("file-type", "PNG image"), { "png" } }, }; diff --git a/src/platform/gui.h b/src/platform/gui.h index a56028d..7b2cdf5 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -329,6 +329,8 @@ struct FileFilter { // SolveSpace's native file format extern std::vector SolveSpaceModelFileFilters; +// SolveSpace's linkable file formats +extern std::vector SolveSpaceLinkFileFilters; // Raster image extern std::vector RasterFileFilters; // Triangle mesh diff --git a/src/solvespace.h b/src/solvespace.h index 795c888..1157723 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -679,6 +679,8 @@ public: void UpgradeLegacyData(); bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); + bool LoadEntitiesFromSlvs(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh); bool ReloadAllLinked(const Platform::Path &filename, bool canCancel = false); // And the various export options void ExportAsPngTo(const Platform::Path &filename); @@ -810,6 +812,7 @@ public: void ImportDxf(const Platform::Path &file); void ImportDwg(const Platform::Path &file); +bool LinkIDF(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); extern SolveSpaceUI SS; extern Sketch SK;