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