1 #include "export_step.hpp"
2 #include "board/board.hpp"
3 #include "board/board_layers.hpp"
4 #include "canvas/canvas_patch.hpp"
5 #include "util/util.hpp"
6 #include "util/geom_util.hpp"
7 #include "pool/ipool.hpp"
8 
9 #include <BRepBuilderAPI_MakeWire.hxx>
10 #include <TDocStd_Document.hxx>
11 #include <XCAFApp_Application.hxx>
12 #include <XCAFDoc_ShapeTool.hxx>
13 #include <TopoDS_Shape.hxx>
14 #include <TopoDS_Edge.hxx>
15 #include <Standard_Version.hxx>
16 
17 
18 #include <IGESCAFControl_Reader.hxx>
19 #include <IGESCAFControl_Writer.hxx>
20 #include <IGESControl_Controller.hxx>
21 #include <IGESData_GlobalSection.hxx>
22 #include <IGESData_IGESModel.hxx>
23 #include <Interface_Static.hxx>
24 #include <Quantity_Color.hxx>
25 #include <STEPCAFControl_Reader.hxx>
26 #include <STEPCAFControl_Writer.hxx>
27 #include <APIHeaderSection_MakeHeader.hxx>
28 #include <TCollection_ExtendedString.hxx>
29 #include <TDataStd_Name.hxx>
30 #include <TDF_LabelSequence.hxx>
31 #include <TDF_ChildIterator.hxx>
32 #include <TopExp_Explorer.hxx>
33 #include <XCAFDoc_DocumentTool.hxx>
34 #include <XCAFDoc_ColorTool.hxx>
35 
36 #include <BRep_Tool.hxx>
37 #include <BRepMesh_IncrementalMesh.hxx>
38 #include <BRepBuilderAPI.hxx>
39 #include <BRepBuilderAPI_MakeEdge.hxx>
40 #include <BRepBuilderAPI_Transform.hxx>
41 #include <BRepBuilderAPI_MakeFace.hxx>
42 #include <BRepPrimAPI_MakePrism.hxx>
43 #include <BRepPrimAPI_MakeCylinder.hxx>
44 #include <BRepPrimAPI_MakeBox.hxx>
45 #include <BRepAlgoAPI_Cut.hxx>
46 
47 #include <TopoDS.hxx>
48 #include <TopoDS_Wire.hxx>
49 #include <TopoDS_Face.hxx>
50 #include <TopoDS_Compound.hxx>
51 #include <TopoDS_Builder.hxx>
52 
53 #include <gp_Ax2.hxx>
54 #include <gp_Circ.hxx>
55 #include <gp_Dir.hxx>
56 #include <gp_Pnt.hxx>
57 
58 #include <glibmm/miscutils.h>
59 
60 namespace horizon {
61 
62 // adapted from https://github.com/KiCad/kicad-source-mirror/blob/master/utils/kicad2step/pcb/oce_utils.cpp
63 
face_from_countour(const ClipperLib::Path & contour)64 static TopoDS_Shape face_from_countour(const ClipperLib::Path &contour)
65 {
66     BRepBuilderAPI_MakeWire wire;
67     auto contour_sz = contour.size();
68     for (size_t i = 0; i < contour_sz; i++) {
69         auto pt1 = contour[i];
70         auto pt2 = contour[(i + 1) % contour_sz];
71         TopoDS_Edge edge;
72         edge = BRepBuilderAPI_MakeEdge(gp_Pnt(pt1.X / 1e6, pt1.Y / 1e6, 0.0), gp_Pnt(pt2.X / 1e6, pt2.Y / 1e6, 0.0));
73         wire.Add(edge);
74     }
75     return BRepBuilderAPI_MakeFace(wire);
76 }
77 
78 class CanvasHole : public Canvas {
79 public:
CanvasHole(TopTools_ListOfShape & cs)80     CanvasHole(TopTools_ListOfShape &cs) : cutouts(cs)
81     {
82         img_mode = true;
83     }
84 
85 private:
86     TopTools_ListOfShape &cutouts;
img_hole(const class Hole & hole)87     void img_hole(const class Hole &hole) override
88     {
89         Placement tr = transform;
90         tr.accumulate(hole.placement);
91         if (hole.shape == Hole::Shape::ROUND) {
92             auto ax = gp_Ax2(gp_Pnt(tr.shift.x / 1e6, tr.shift.y / 1e6, 0), gp_Dir(0, 0, 1));
93             auto circ = gp_Circ(ax, hole.diameter / 2e6);
94             auto edge = BRepBuilderAPI_MakeEdge(circ);
95             BRepBuilderAPI_MakeWire wire;
96             wire.Add(edge);
97             TopoDS_Shape face = BRepBuilderAPI_MakeFace(wire);
98             cutouts.Append(face);
99         }
100         else if (hole.shape == Hole::Shape::SLOT) {
101             const int64_t box_width = hole.length - hole.diameter;
102             {
103                 std::array<Coordd, 4> corners;
104                 corners.at(0) = {box_width / -2.0, hole.diameter / -2.0};
105                 corners.at(1) = {box_width / -2.0, hole.diameter / 2.0};
106                 corners.at(2) = {box_width / 2.0, hole.diameter / 2.0};
107                 corners.at(3) = {box_width / 2.0, hole.diameter / -2.0};
108 
109                 BRepBuilderAPI_MakeWire wire;
110                 for (size_t i = 0; i < corners.size(); i++) {
111                     auto pt1 = tr.transform(corners.at(i));
112                     auto pt2 = tr.transform(corners.at((i + 1) % corners.size()));
113                     TopoDS_Edge edge;
114                     edge = BRepBuilderAPI_MakeEdge(gp_Pnt(pt1.x / 1e6, pt1.y / 1e6, 0.0),
115                                                    gp_Pnt(pt2.x / 1e6, pt2.y / 1e6, 0.0));
116                     wire.Add(edge);
117                 }
118                 TopoDS_Shape face = BRepBuilderAPI_MakeFace(wire);
119                 cutouts.Append(face);
120             }
121 
122             for (int mul : {1, -1}) {
123                 auto hole_pos = tr.transform(Coordi(mul * box_width / 2, 0));
124                 auto ax = gp_Ax2(gp_Pnt(hole_pos.x / 1e6, hole_pos.y / 1e6, 0), gp_Dir(0, 0, 1));
125                 auto circ = gp_Circ(ax, hole.diameter / 2e6);
126                 auto edge = BRepBuilderAPI_MakeEdge(circ);
127                 BRepBuilderAPI_MakeWire wire;
128                 wire.Add(edge);
129                 TopoDS_Shape face = BRepBuilderAPI_MakeFace(wire);
130                 cutouts.Append(face);
131             }
132         }
133     }
134 
push()135     void push() override
136     {
137     }
request_push()138     void request_push() override
139     {
140     }
141 };
142 
143 struct DOUBLET {
144     double x;
145     double y;
146 
DOUBLEThorizon::DOUBLET147     DOUBLET() : x(0.0), y(0.0)
148     {
149         return;
150     }
DOUBLEThorizon::DOUBLET151     DOUBLET(double aX, double aY) : x(aX), y(aY)
152     {
153         return;
154     }
155 };
156 
157 struct TRIPLET {
158     double x;
159     double y;
160 
161     union {
162         double z;
163         double angle;
164     };
165 
TRIPLEThorizon::TRIPLET166     TRIPLET() : x(0.0), y(0.0), z(0.0)
167     {
168         return;
169     }
TRIPLEThorizon::TRIPLET170     TRIPLET(double aX, double aY, double aZ) : x(aX), y(aY), z(aZ)
171     {
172         return;
173     }
174 };
175 
176 #define BOARD_OFFSET (0.05)
177 
getModelLocation(bool aBottom,DOUBLET aPosition,double aRotation,TRIPLET aOffset,TRIPLET aOrientation,TopLoc_Location & aLocation,double board_thickness)178 static bool getModelLocation(bool aBottom, DOUBLET aPosition, double aRotation, TRIPLET aOffset, TRIPLET aOrientation,
179                              TopLoc_Location &aLocation, double board_thickness)
180 {
181     // Order of operations:
182     // a. aOrientation is applied -Z*-Y*-X
183     // b. aOffset is applied
184     //      Top ? add thickness to the Z offset
185     // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
186     //             then rotate on +Z
187     //    Top ? rotate on -Z
188     // d. aPosition is applied
189     //
190     // Note: Y axis is inverted in KiCad
191 
192     gp_Trsf lPos;
193     lPos.SetTranslation(gp_Vec(aPosition.x, -aPosition.y, 0.0));
194 
195     // Offset board thickness
196     aOffset.z += BOARD_OFFSET;
197 
198     gp_Trsf lRot;
199 
200     if (aBottom) {
201         lRot.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), aRotation + M_PI);
202         lPos.Multiply(lRot);
203         lRot.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0)), M_PI);
204         lPos.Multiply(lRot);
205     }
206     else {
207         aOffset.z += board_thickness;
208         lRot.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), aRotation);
209         lPos.Multiply(lRot);
210     }
211 
212     gp_Trsf lOff;
213     lOff.SetTranslation(gp_Vec(aOffset.x, aOffset.y, aOffset.z));
214     lPos.Multiply(lOff);
215 
216     gp_Trsf lOrient;
217     lOrient.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(1.0, 0.0, 0.0)), -aOrientation.x);
218     lPos.Multiply(lOrient);
219     lOrient.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0)), -aOrientation.y);
220     lPos.Multiply(lOrient);
221     lOrient.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), -aOrientation.z);
222     lPos.Multiply(lOrient);
223 
224     aLocation = TopLoc_Location(lPos);
225     return true;
226 }
227 
228 #define USER_PREC (1e-4)
229 
readSTEP(Handle (TDocStd_Document)& doc,const char * fname)230 static bool readSTEP(Handle(TDocStd_Document) & doc, const char *fname)
231 {
232     STEPCAFControl_Reader reader;
233     IFSelect_ReturnStatus stat = reader.ReadFile(fname);
234 
235     if (stat != IFSelect_RetDone)
236         return false;
237 
238     // Enable user-defined shape precision
239     if (!Interface_Static::SetIVal("read.precision.mode", 1))
240         return false;
241 
242     // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
243     if (!Interface_Static::SetRVal("read.precision.val", USER_PREC))
244         return false;
245 
246     // set other translation options
247     reader.SetColorMode(true);  // use model colors
248     reader.SetNameMode(false);  // don't use label names
249     reader.SetLayerMode(false); // ignore LAYER data
250 
251     if (!reader.Transfer(doc)) {
252         doc->Close();
253         return false;
254     }
255 
256     // are there any shapes to translate?
257     if (reader.NbRootsForTransfer() < 1) {
258         doc->Close();
259         return false;
260     }
261 
262     return true;
263 }
264 
transferModel(Handle (TDocStd_Document)& source,Handle (TDocStd_Document)& dest,const std::string & name)265 static TDF_Label transferModel(Handle(TDocStd_Document) & source, Handle(TDocStd_Document) & dest,
266                                const std::string &name)
267 {
268     // transfer data from Source into a top level component of Dest
269 
270     // s_assy = shape tool for the source
271     Handle(XCAFDoc_ShapeTool) s_assy = XCAFDoc_DocumentTool::ShapeTool(source->Main());
272 
273     // retrieve all free shapes within the assembly
274     TDF_LabelSequence frshapes;
275     s_assy->GetFreeShapes(frshapes);
276 
277     // d_assy = shape tool for the destination
278     Handle(XCAFDoc_ShapeTool) d_assy = XCAFDoc_DocumentTool::ShapeTool(dest->Main());
279 
280     // create a new shape within the destination and set the assembly tool to point to it
281     TDF_Label component = d_assy->NewShape();
282 
283     int nshapes = frshapes.Length();
284     int id = 1;
285     Handle(XCAFDoc_ColorTool) scolor = XCAFDoc_DocumentTool::ColorTool(source->Main());
286     Handle(XCAFDoc_ColorTool) dcolor = XCAFDoc_DocumentTool::ColorTool(dest->Main());
287     TopExp_Explorer dtop;
288     TopExp_Explorer stop;
289 
290     while (id <= nshapes) {
291         TopoDS_Shape shape = s_assy->GetShape(frshapes.Value(id));
292 
293         if (!shape.IsNull()) {
294             TDF_Label la = d_assy->AddShape(shape, false);
295             TDF_Label niulab = d_assy->AddComponent(component, la, shape.Location());
296             TDataStd_Name::Set(la, (name + "-" + std::to_string(id)).c_str());
297 
298             // check for per-surface colors
299             stop.Init(shape, TopAbs_FACE);
300             dtop.Init(d_assy->GetShape(niulab), TopAbs_FACE);
301 
302             while (stop.More() && dtop.More()) {
303                 Quantity_Color face_color;
304 
305                 TDF_Label tl;
306 
307                 // give priority to the base shape's color
308                 if (s_assy->FindShape(stop.Current(), tl)) {
309                     if (scolor->GetColor(tl, XCAFDoc_ColorSurf, face_color)
310                         || scolor->GetColor(tl, XCAFDoc_ColorGen, face_color)
311                         || scolor->GetColor(tl, XCAFDoc_ColorCurv, face_color)) {
312                         dcolor->SetColor(dtop.Current(), face_color, XCAFDoc_ColorSurf);
313                     }
314                 }
315                 else if (scolor->GetColor(stop.Current(), XCAFDoc_ColorSurf, face_color)
316                          || scolor->GetColor(stop.Current(), XCAFDoc_ColorGen, face_color)
317                          || scolor->GetColor(stop.Current(), XCAFDoc_ColorCurv, face_color)) {
318 
319                     dcolor->SetColor(dtop.Current(), face_color, XCAFDoc_ColorSurf);
320                 }
321 
322                 stop.Next();
323                 dtop.Next();
324             }
325 
326             // check for per-solid colors
327             stop.Init(shape, TopAbs_SOLID);
328             dtop.Init(d_assy->GetShape(niulab), TopAbs_SOLID, TopAbs_FACE);
329 
330             while (stop.More() && dtop.More()) {
331                 Quantity_Color face_color;
332 
333                 TDF_Label tl;
334 
335                 // give priority to the base shape's color
336                 if (s_assy->FindShape(stop.Current(), tl)) {
337                     if (scolor->GetColor(tl, XCAFDoc_ColorSurf, face_color)
338                         || scolor->GetColor(tl, XCAFDoc_ColorGen, face_color)
339                         || scolor->GetColor(tl, XCAFDoc_ColorCurv, face_color)) {
340                         dcolor->SetColor(dtop.Current(), face_color, XCAFDoc_ColorGen);
341                     }
342                 }
343                 else if (scolor->GetColor(stop.Current(), XCAFDoc_ColorSurf, face_color)
344                          || scolor->GetColor(stop.Current(), XCAFDoc_ColorGen, face_color)
345                          || scolor->GetColor(stop.Current(), XCAFDoc_ColorCurv, face_color)) {
346                     dcolor->SetColor(dtop.Current(), face_color, XCAFDoc_ColorSurf);
347                 }
348 
349                 stop.Next();
350                 dtop.Next();
351             }
352         }
353 
354         ++id;
355     };
356 
357     return component;
358 }
359 
getModelLabel(const std::string & aFileName,TDF_Label & aLabel,Handle (XCAFApp_Application)app,Handle (TDocStd_Document)doc,const std::string & name)360 static bool getModelLabel(const std::string &aFileName, TDF_Label &aLabel, Handle(XCAFApp_Application) app,
361                           Handle(TDocStd_Document) doc, const std::string &name)
362 {
363     Handle(TDocStd_Document) my_doc;
364     app->NewDocument("MDTV-XCAF", my_doc);
365     if (!readSTEP(my_doc, aFileName.c_str())) {
366         throw std::runtime_error("error loading step");
367     }
368 
369     aLabel = transferModel(my_doc, doc, name);
370 
371     TCollection_ExtendedString partname(name.c_str());
372     TDataStd_Name::Set(aLabel, partname);
373 
374     return !aLabel.IsNull();
375 }
376 
export_step(const std::string & filename,const Board & brd,class IPool & pool,bool include_models,std::function<void (const std::string &)> progress_cb,const BoardColors * colors,const std::string & prefix)377 void export_step(const std::string &filename, const Board &brd, class IPool &pool, bool include_models,
378                  std::function<void(const std::string &)> progress_cb, const BoardColors *colors,
379                  const std::string &prefix)
380 {
381     auto app = XCAFApp_Application::GetApplication();
382     Handle(TDocStd_Document) doc;
383     app->NewDocument("MDTV-XCAF", doc);
384     XCAFDoc_ShapeTool::SetAutoNaming(false);
385     BRepBuilderAPI::Precision(1.0e-6);
386     auto assy = XCAFDoc_DocumentTool::ShapeTool(doc->Main());
387     auto assy_label = assy->NewShape();
388     TDataStd_Name::Set(assy_label, (prefix + "PCA").c_str());
389 
390     progress_cb("Board outline…");
391     ClipperLib::Clipper cl;
392     {
393         CanvasPatch canvas;
394         canvas.update(brd);
395         for (const auto &it : canvas.get_patches()) {
396             if (it.first.layer == BoardLayers::L_OUTLINE) {
397                 cl.AddPaths(it.second, ClipperLib::ptSubject, true);
398             }
399         }
400     }
401     ClipperLib::PolyTree result;
402     cl.Execute(ClipperLib::ctUnion, result, ClipperLib::pftEvenOdd);
403 
404     if (result.ChildCount() != 1) {
405         throw std::runtime_error("invalid board outline");
406     }
407 
408     auto outline = result.Childs.front()->Contour;
409     if (outline.size() < 3) {
410         throw std::runtime_error("outline has less than 3 vertices");
411     }
412 
413     int64_t total_thickness = 0;
414     for (const auto &it : brd.stackup) {
415         if (it.second.layer != BoardLayers::BOTTOM_COPPER) {
416             total_thickness += it.second.substrate_thickness;
417         }
418     }
419 
420     progress_cb("Board cutouts…");
421     TopTools_ListOfShape cutouts;
422     for (const auto &hole_node : result.Childs.front()->Childs) {
423         auto hole_outline = hole_node->Contour;
424         cutouts.Append(face_from_countour(hole_outline));
425     }
426 
427     progress_cb("Holes…");
428     {
429         CanvasHole canvas_hole(cutouts);
430         canvas_hole.update(brd);
431     }
432 
433     progress_cb("Creating board…");
434     TopoDS_Shape board;
435     {
436         TopoDS_Shape board_face = face_from_countour(outline);
437         BRepAlgoAPI_Cut builder;
438 
439         TopTools_ListOfShape board_shapes;
440         board_shapes.Append(board_face);
441 
442         builder.SetArguments(board_shapes);
443         builder.SetTools(cutouts);
444         builder.SetRunParallel(Standard_True);
445         builder.Build();
446 
447         board = BRepPrimAPI_MakePrism(builder.Shape(), gp_Vec(0, 0, total_thickness / 1e6));
448     }
449 
450     TDF_Label board_label = assy->AddShape(board, false);
451     assy->AddComponent(assy_label, board_label, board.Location());
452     TDataStd_Name::Set(board_label, (prefix + "PCB").c_str());
453 
454     if (!board_label.IsNull()) {
455         Handle(XCAFDoc_ColorTool) color = XCAFDoc_DocumentTool::ColorTool(doc->Main());
456         Color c;
457         if (colors) {
458             c = colors->solder_mask;
459         }
460         else {
461             c = brd.colors.solder_mask;
462         }
463         Quantity_Color pcb_color(c.r, c.g, c.b, Quantity_TOC_RGB);
464         color->SetColor(board_label, pcb_color, XCAFDoc_ColorSurf);
465         TopExp_Explorer topex;
466         topex.Init(assy->GetShape(board_label), TopAbs_SOLID);
467 
468         while (topex.More()) {
469             color->SetColor(topex.Current(), pcb_color, XCAFDoc_ColorSurf);
470             topex.Next();
471         }
472     }
473 
474     if (include_models) {
475         progress_cb("Packages…");
476         size_t i = 1;
477         std::vector<const BoardPackage *> pkgs;
478         pkgs.reserve(brd.packages.size());
479         for (const auto &[uuid, package] : brd.packages) {
480             if (package.component && package.component->nopopulate) {
481                 continue;
482             }
483             pkgs.push_back(&package);
484         }
485         auto n_pkg = std::to_string(pkgs.size());
486         std::sort(pkgs.begin(), pkgs.end(),
487                   [](auto a, auto b) { return strcmp_natural(a->component->refdes, b->component->refdes) < 0; });
488 
489         for (const auto it : pkgs) {
490             try {
491                 auto model = it->package.get_model(it->model);
492                 if (model) {
493                     progress_cb("Package " + it->component->refdes + " (" + std::to_string(i) + "/" + n_pkg + ")");
494                     TDF_Label lmodel;
495 
496                     if (!getModelLabel(pool.get_model_filename(it->package.uuid, model->uuid), lmodel, app, doc,
497                                        prefix + it->component->refdes)) {
498                         throw std::runtime_error("get model label");
499                     }
500 
501                     TopLoc_Location toploc;
502                     DOUBLET pos(it->placement.shift.x / 1e6, it->placement.shift.y / -1e6);
503                     double rot = angle_to_rad(it->placement.get_angle());
504                     TRIPLET offset(model->x / 1e6, model->y / 1e6, model->z / 1e6);
505                     TRIPLET orientation(angle_to_rad(model->roll), angle_to_rad(model->pitch),
506                                         angle_to_rad(model->yaw));
507                     getModelLocation(it->flip, pos, rot, offset, orientation, toploc, total_thickness / 1e6);
508 
509                     assy->AddComponent(assy_label, lmodel, toploc);
510 
511                     TCollection_ExtendedString refdes((prefix + it->component->refdes).c_str());
512                     TDataStd_Name::Set(lmodel, refdes);
513                 }
514             }
515             catch (const std::exception &e) {
516                 progress_cb("Error processing package " + it->component->refdes + ": " + e.what());
517             }
518             i++;
519         }
520     }
521     progress_cb("Writing output file");
522 #if OCC_VERSION_MAJOR >= 7 && OCC_VERSION_MINOR >= 2
523     assy->UpdateAssemblies();
524 #endif
525     STEPCAFControl_Writer writer;
526     writer.SetColorMode(Standard_True);
527     writer.SetNameMode(Standard_True);
528     if (Standard_False == writer.Transfer(doc, STEPControl_AsIs)) {
529         throw std::runtime_error("transfer error");
530     }
531 
532     APIHeaderSection_MakeHeader hdr(writer.ChangeWriter().Model());
533     hdr.SetName(new TCollection_HAsciiString("Board"));
534     hdr.SetAuthorValue(1, new TCollection_HAsciiString("An Author"));
535     hdr.SetOrganizationValue(1, new TCollection_HAsciiString("A Company"));
536     hdr.SetOriginatingSystem(new TCollection_HAsciiString("horizon EDA"));
537     hdr.SetDescriptionValue(1, new TCollection_HAsciiString("Electronic assembly"));
538 
539     if (Standard_False == writer.Write(filename.c_str()))
540         throw std::runtime_error("write error");
541 
542     progress_cb("Done");
543 }
544 } // namespace horizon
545