KiCad PCB EDA Suite
Loading...
Searching...
No Matches
step_pcb_model.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2022 Mark Roszko <[email protected]>
5 * Copyright (C) 2016 Cirilo Bernardo <[email protected]>
6 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, you may find one here:
20 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21 * or you may search the http://www.gnu.org website for the version 2 license,
22 * or you may write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26#include <algorithm>
27#include <cmath>
28#include <sstream>
29#include <string>
30#include <utility>
31#include <wx/file.h>
32#include <wx/filename.h>
33#include <wx/filefn.h>
34#include <wx/sstream.h>
35#include <wx/stdpaths.h>
36#include <wx/stream.h>
37#include <wx/string.h>
38#include <wx/zstream.h>
39#include <wx/wfstream.h>
40#include <wx/zipstrm.h>
41#include <wx/stdstream.h>
42#include <wx/crt.h>
43
44#include <decompress.hpp>
45
46#include <thread_pool.h>
47#include <trace_helpers.h>
48#include <board.h>
50#include <footprint.h>
51#include <pad.h>
52#include <pcb_track.h>
53#include <kiplatform/io.h>
54#include <string_utils.h>
55#include <build_version.h>
60#include <reporter.h>
61
63
65#include <pcb_painter.h>
66
67#include "step_pcb_model.h"
68#include "streamwrapper.h"
69
70#include <IGESCAFControl_Reader.hxx>
71#include <IGESCAFControl_Writer.hxx>
72#include <IGESControl_Controller.hxx>
73#include <IGESData_GlobalSection.hxx>
74#include <IGESData_IGESModel.hxx>
75#include <Interface_Static.hxx>
76#include <Quantity_Color.hxx>
77#include <STEPCAFControl_Reader.hxx>
78#include <STEPCAFControl_Writer.hxx>
79#include <APIHeaderSection_MakeHeader.hxx>
80#include <Standard_Failure.hxx>
81#include <Standard_Handle.hxx>
82#include <Standard_Version.hxx>
83#include <TCollection_ExtendedString.hxx>
84#include <TDocStd_Document.hxx>
85#include <TDocStd_XLinkTool.hxx>
86#include <TDataStd_Name.hxx>
87#include <TDataStd_TreeNode.hxx>
88#include <TDF_LabelSequence.hxx>
89#include <TDF_Tool.hxx>
90#include <TopExp_Explorer.hxx>
91#include <TopoDS.hxx>
92#include <XCAFApp_Application.hxx>
93#include <XCAFDoc.hxx>
94#include <XCAFDoc_DocumentTool.hxx>
95#include <XCAFDoc_ColorTool.hxx>
96#include <XCAFDoc_ShapeTool.hxx>
97#include <XCAFDoc_VisMaterialTool.hxx>
98#include <XCAFDoc_Area.hxx>
99#include <XCAFDoc_Centroid.hxx>
100#include <XCAFDoc_Location.hxx>
101#include <XCAFDoc_Volume.hxx>
102#include "kicad3d_info.h"
103
104#include "KI_XCAFDoc_AssemblyGraph.hxx"
105
106#include <BRep_Tool.hxx>
107#include <BRepMesh_IncrementalMesh.hxx>
108#include <BRepBuilderAPI_GTransform.hxx>
109#include <BRepBuilderAPI_MakeEdge.hxx>
110#include <BRepBuilderAPI_MakeWire.hxx>
111#include <BRepBuilderAPI_MakeFace.hxx>
112#include <BRepExtrema_DistShapeShape.hxx>
113#include <BRepPrimAPI_MakeCone.hxx>
114#include <BRepPrimAPI_MakeCylinder.hxx>
115#include <BRepPrimAPI_MakePrism.hxx>
116#include <BRepTools.hxx>
117#include <BRepLib_MakeWire.hxx>
118#include <BRepAdaptor_Surface.hxx>
119#include <BRepAlgoAPI_Check.hxx>
120#include <BRepAlgoAPI_Cut.hxx>
121#include <BRepAlgoAPI_Fuse.hxx>
122#include <ShapeUpgrade_UnifySameDomain.hxx>
123
124#include <BRepBndLib.hxx>
125#include <Bnd_BoundSortBox.hxx>
126#include <GProp_GProps.hxx>
127#include <BRepGProp.hxx>
128
129#include <Geom_Curve.hxx>
130#include <Geom_TrimmedCurve.hxx>
131
132#include <gp_Ax2.hxx>
133#include <gp_Dir.hxx>
134#include <gp_Pnt.hxx>
135#include <GC_MakeArcOfCircle.hxx>
136#include <GC_MakeCircle.hxx>
137
138#include <RWGltf_CafWriter.hxx>
139#include <StlAPI_Writer.hxx>
140
141#if OCC_VERSION_HEX >= 0x070700
142#include <VrmlAPI_CafReader.hxx>
143#include <RWPly_CafWriter.hxx>
144#endif
145
146#include <macros.h>
148
149static constexpr double USER_PREC = 1e-4;
150static constexpr double USER_ANGLE_PREC = 1e-6;
151
152// nominal offset from the board
153static constexpr double BOARD_OFFSET = 0.05;
154
155// supported file types for 3D models
167
168
169MODEL3D_FORMAT_TYPE fileType( const char* aFileName )
170{
171 wxFileName lfile( wxString::FromUTF8Unchecked( aFileName ) );
172
173 if( !lfile.FileExists() )
174 return FMT_NONE;
175
176 wxString ext = lfile.GetExt().Lower();
177
178 if( ext == wxT( "wrl" ) )
179 return FMT_WRL;
180
181 if( ext == wxT( "wrz" ) )
182 return FMT_WRZ;
183
184 if( ext == wxT( "idf" ) )
185 return FMT_IDF; // component outline
186
187 if( ext == wxT( "emn" ) )
188 return FMT_EMN; // PCB assembly
189
190 if( ext == wxT( "stpz" ) || ext == wxT( "gz" ) )
191 return FMT_STEPZ;
192
193 OPEN_ISTREAM( ifile, aFileName );
194
195 if( ifile.fail() )
196 return FMT_NONE;
197
198 char iline[82];
199 MODEL3D_FORMAT_TYPE format_type = FMT_NONE;
200
201 // The expected header should be the first line.
202 // However some files can have a comment at the beginning of the file
203 // So read up to max_line_count lines to try to find the actual header
204 const int max_line_count = 3;
205
206 for( int ii = 0; ii < max_line_count; ii++ )
207 {
208 memset( iline, 0, 82 );
209 ifile.getline( iline, 82 );
210
211 iline[81] = 0; // ensure NULL termination when string is too long
212
213 // check for STEP in Part 21 format
214 // (this can give false positives since Part 21 is not exclusively STEP)
215 if( !strncmp( iline, "ISO-10303-21;", 13 ) )
216 {
217 format_type = FMT_STEP;
218 break;
219 }
220
221 std::string fstr = iline;
222
223 // check for STEP in XML format
224 // (this can give both false positive and false negatives)
225 if( fstr.find( "urn:oid:1.0.10303." ) != std::string::npos )
226 {
227 format_type = FMT_STEP;
228 break;
229 }
230
231 // Note: this is a very simple test which can yield false positives; the only
232 // sure method for determining if a file *not* an IGES model is to attempt
233 // to load it.
234 if( iline[72] == 'S' && ( iline[80] == 0 || iline[80] == 13 || iline[80] == 10 ) )
235 {
236 format_type = FMT_IGES;
237 break;
238 }
239
240 // Only a comment (starting by "/*") is allowed as header
241 if( strncmp( iline, "/*", 2 ) != 0 ) // not a comment
242 break;
243 }
244
245 CLOSE_STREAM( ifile );
246
247 return format_type;
248}
249
250
252 const VECTOR2D& p3 )
253{
255
256 // Move coordinate origin to p2, to simplify calculations
257 VECTOR2D b = p1 - p2;
258 VECTOR2D d = p3 - p2;
259 double bc = ( b.x * b.x + b.y * b.y ) / 2.0;
260 double cd = ( -d.x * d.x - d.y * d.y ) / 2.0;
261 double det = -b.x * d.y + d.x * b.y;
262
263 // We're fine with divisions by 0
264 det = 1.0 / det;
265 center.x = ( -bc * d.y - cd * b.y ) * det;
266 center.y = ( b.x * cd + d.x * bc ) * det;
267 center += p2;
268
269 return center;
270}
271
272
273#define APPROX_DBG( stmt )
274//#define APPROX_DBG( stmt ) stmt
275
277{
278 // An algo that takes 3 points, calculates a circle center,
279 // then tries to find as many points fitting the circle.
280
281 static const double c_radiusDeviation = 1000.0;
282 static const double c_arcCenterDeviation = 1000.0;
283 static const double c_relLengthDeviation = 0.8;
284 static const int c_last_none = -1000; // Meaning the arc cannot be constructed
285 // Allow larger angles for segments below this size
286 static const double c_smallSize = pcbIUScale.mmToIU( 0.1 );
287 static const double c_circleCloseGap = pcbIUScale.mmToIU( 1.0 );
288
289 APPROX_DBG( std::cout << std::endl );
290
291 if( aSrc.PointCount() < 4 )
292 return aSrc;
293
294 if( !aSrc.IsClosed() )
295 return aSrc; // non-closed polygons are not supported
296
298
299 int jEndIdx = aSrc.PointCount() - 3;
300
301 for( int i = 0; i < aSrc.PointCount(); i++ )
302 {
303 int first = i - 3;
304 int last = c_last_none;
305
306 VECTOR2D p0 = aSrc.CPoint( i - 3 );
307 VECTOR2D p1 = aSrc.CPoint( i - 2 );
308 VECTOR2D p2 = aSrc.CPoint( i - 1 );
309
310 APPROX_DBG( std::cout << i << " " << aSrc.CPoint( i ) << " " << ( i - 3 ) << " "
311 << VECTOR2I( p0 ) << " " << ( i - 2 ) << " " << VECTOR2I( p1 ) << " "
312 << ( i - 1 ) << " " << VECTOR2I( p2 ) << std::endl );
313
314 VECTOR2D v01 = p1 - p0;
315 VECTOR2D v12 = p2 - p1;
316
317 bool defective = false;
318
319 double d01 = v01.EuclideanNorm();
320 double d12 = v12.EuclideanNorm();
321
322 // Check distance differences between 3 first points
323 defective |= std::abs( d01 - d12 ) > ( std::max( d01, d12 ) * c_relLengthDeviation );
324
325 if( !defective )
326 {
327 // Check angles between 3 first points
328 EDA_ANGLE a01( v01 );
329 EDA_ANGLE a12( v12 );
330
331 double a_diff = ( a01 - a12 ).Normalize180().AsDegrees();
332 defective |= std::abs( a_diff ) < 0.1;
333
334 // Larger angles are allowed for smaller geometry
335 double maxAngleDiff = std::max( d01, d12 ) < c_smallSize ? 46.0 : 30.0;
336 defective |= std::abs( a_diff ) >= maxAngleDiff;
337 }
338
339 if( !defective )
340 {
341 // Find last point lying on the circle created from 3 first points
343 double radius = ( p0 - center ).EuclideanNorm();
344 VECTOR2D p_prev = p2;
345 EDA_ANGLE a_prev( v12 );
346
347 for( int j = i; j <= jEndIdx; j++ )
348 {
349 VECTOR2D p_test = aSrc.CPoint( j );
350
351 EDA_ANGLE a_test( p_test - p_prev );
352 double rad_test = ( p_test - center ).EuclideanNorm();
353 double d_tl = ( p_test - p_prev ).EuclideanNorm();
354 double rad_dev = std::abs( radius - rad_test );
355
356 APPROX_DBG( std::cout << " " << j << " " << aSrc.CPoint( j ) << " rad "
357 << int64_t( rad_test ) << " ref " << int64_t( radius )
358 << std::endl );
359
360 if( rad_dev > c_radiusDeviation )
361 {
362 APPROX_DBG( std::cout << " " << j
363 << " Radius deviation too large: " << int64_t( rad_dev )
364 << " > " << c_radiusDeviation << std::endl );
365 break;
366 }
367
368 // Larger angles are allowed for smaller geometry
369 double maxAngleDiff =
370 std::max( std::max( d01, d12 ), d_tl ) < c_smallSize ? 46.0 : 30.0;
371
372 double a_diff_test = ( a_prev - a_test ).Normalize180().AsDegrees();
373 if( std::abs( a_diff_test ) >= maxAngleDiff )
374 {
375 APPROX_DBG( std::cout << " " << j << " Angles differ too much " << a_diff_test
376 << std::endl );
377 break;
378 }
379
380 if( std::abs( d_tl - d01 ) > ( std::max( d_tl, d01 ) * c_relLengthDeviation ) )
381 {
382 APPROX_DBG( std::cout << " " << j << " Lengths differ too much " << d_tl
383 << "; " << d01 << std::endl );
384 break;
385 }
386
387 last = j;
388 p_prev = p_test;
389 a_prev = a_test;
390 }
391 }
392
393 if( last != c_last_none )
394 {
395 // Try to add an arc, testing for self-interference
396 SHAPE_ARC arc( aSrc.CPoint( first ), aSrc.CPoint( ( first + last ) / 2 ),
397 aSrc.CPoint( last ), 0 );
398
399 if( last > aSrc.PointCount() - 3 && !dst.IsArcSegment( 0 ) )
400 {
401 // If we've found an arc at the end, but already added segments at the start, remove them.
402 int toRemove = last - ( aSrc.PointCount() - 3 );
403
404 while( toRemove )
405 {
406 dst.RemoveShape( 0 );
407 toRemove--;
408 }
409 }
410
411 SHAPE_LINE_CHAIN testChain = dst;
412
413 testChain.Append( arc );
414 testChain.Append( aSrc.Slice( last, std::max( last, aSrc.PointCount() - 3 ) ) );
415 testChain.SetClosed( aSrc.IsClosed() );
416
417 if( !testChain.SelfIntersectingWithArcs() )
418 {
419 // Add arc
420 dst.Append( arc );
421
422 APPROX_DBG( std::cout << " Add arc start " << arc.GetP0() << " mid "
423 << arc.GetArcMid() << " end " << arc.GetP1() << std::endl );
424
425 i = last + 3;
426 }
427 else
428 {
429 // Self-interference
430 last = c_last_none;
431
432 APPROX_DBG( std::cout << " Self-intersection check failed" << std::endl );
433 }
434 }
435
436 if( last == c_last_none )
437 {
438 if( first < 0 )
439 jEndIdx = first + aSrc.PointCount();
440
441 // Add point
442 dst.Append( p0 );
443 APPROX_DBG( std::cout << " Add pt " << VECTOR2I( p0 ) << std::endl );
444 }
445 }
446
447 dst.SetClosed( true );
448
449 // Try to merge arcs
450 int iarc0 = dst.ArcIndex( 0 );
451 int iarc1 = dst.ArcIndex( dst.GetSegmentCount() - 1 );
452
453 if( iarc0 != -1 && iarc1 != -1 )
454 {
455 APPROX_DBG( std::cout << "Final arcs " << iarc0 << " " << iarc1 << std::endl );
456
457 if( iarc0 == iarc1 )
458 {
459 SHAPE_ARC arc = dst.Arc( iarc0 );
460
461 VECTOR2D p0 = arc.GetP0();
462 VECTOR2D p1 = arc.GetP1();
463
464 // If we have only one arc and the gap is small, make it a circle
465 if( ( p1 - p0 ).EuclideanNorm() < c_circleCloseGap )
466 {
467 dst.Clear();
468 dst.Append( SHAPE_ARC( arc.GetCenter(), arc.GetP0(), ANGLE_360 ) );
469 }
470 }
471 else
472 {
473 // Merge first and last arcs if they are similar
474 SHAPE_ARC arc0 = dst.Arc( iarc0 );
475 SHAPE_ARC arc1 = dst.Arc( iarc1 );
476
477 VECTOR2D ac0 = arc0.GetCenter();
478 VECTOR2D ac1 = arc1.GetCenter();
479
480 double ar0 = arc0.GetRadius();
481 double ar1 = arc1.GetRadius();
482
483 if( std::abs( ar0 - ar1 ) <= c_radiusDeviation
484 && ( ac0 - ac1 ).EuclideanNorm() <= c_arcCenterDeviation )
485 {
486 dst.RemoveShape( 0 );
487 dst.RemoveShape( -1 );
488
489 SHAPE_ARC merged( arc1.GetP0(), arc1.GetArcMid(), arc0.GetP1(), 0 );
490
491 dst.Append( merged );
492 }
493 }
494 }
495
496 return dst;
497}
498
499
500static TopoDS_Shape getOneShape( Handle( XCAFDoc_ShapeTool ) aShapeTool )
501{
502 TDF_LabelSequence theLabels;
503 aShapeTool->GetFreeShapes( theLabels );
504
505 TopoDS_Shape aShape;
506
507 if( theLabels.Length() == 1 )
508 return aShapeTool->GetShape( theLabels.Value( 1 ) );
509
510 TopoDS_Compound aCompound;
511 BRep_Builder aBuilder;
512 aBuilder.MakeCompound( aCompound );
513
514 for( TDF_LabelSequence::Iterator anIt( theLabels ); anIt.More(); anIt.Next() )
515 {
516 TopoDS_Shape aFreeShape;
517
518 if( !aShapeTool->GetShape( anIt.Value(), aFreeShape ) )
519 continue;
520
521 aBuilder.Add( aCompound, aFreeShape );
522 }
523
524 if( aCompound.NbChildren() > 0 )
525 aShape = aCompound;
526
527 return aShape;
528}
529
530
531// Apply scaling to shapes within theLabel.
532// Based on XCAFDoc_Editor::RescaleGeometry
533static Standard_Boolean rescaleShapes( const TDF_Label& theLabel, const gp_XYZ& aScale )
534{
535 if( theLabel.IsNull() )
536 {
537 Message::SendFail( "Null label." );
538 return Standard_False;
539 }
540
541 if( Abs( aScale.X() ) <= gp::Resolution() || Abs( aScale.Y() ) <= gp::Resolution()
542 || Abs( aScale.Z() ) <= gp::Resolution() )
543 {
544 Message::SendFail( "Scale factor is too small." );
545 return Standard_False;
546 }
547
548 Handle( XCAFDoc_ShapeTool ) aShapeTool = XCAFDoc_DocumentTool::ShapeTool( theLabel );
549
550 if( aShapeTool.IsNull() )
551 {
552 Message::SendFail( "Couldn't find XCAFDoc_ShapeTool attribute." );
553 return Standard_False;
554 }
555
556 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( theLabel );
557
558 if( aG.IsNull() )
559 {
560 Message::SendFail( "Couldn't create assembly graph." );
561 return Standard_False;
562 }
563
564 Standard_Boolean anIsDone = Standard_True;
565
566 // clang-format off
567 gp_GTrsf aGTrsf;
568 aGTrsf.SetVectorialPart( gp_Mat( aScale.X(), 0, 0,
569 0, aScale.Y(), 0,
570 0, 0, aScale.Z() ) );
571 // clang-format on
572
573 BRepBuilderAPI_GTransform aBRepTrsf( aGTrsf );
574
575 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
576 {
577 const KI_XCAFDoc_AssemblyGraph::NodeType aNodeType = aG->GetNodeType( idx );
578
579 if( ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Part )
580 && ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence ) )
581 {
582 continue;
583 }
584
585 const TDF_Label& aLabel = aG->GetNode( idx );
586
587 if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Part )
588 {
589 const TopoDS_Shape aShape = aShapeTool->GetShape( aLabel );
590 aBRepTrsf.Perform( aShape, Standard_True );
591 if( !aBRepTrsf.IsDone() )
592 {
593 Standard_SStream aSS;
594 TCollection_AsciiString anEntry;
595 TDF_Tool::Entry( aLabel, anEntry );
596 aSS << "Shape " << anEntry << " is not scaled!";
597 Message::SendFail( aSS.str().c_str() );
598 anIsDone = Standard_False;
599 return Standard_False;
600 }
601 TopoDS_Shape aScaledShape = aBRepTrsf.Shape();
602 aShapeTool->SetShape( aLabel, aScaledShape );
603
604 // Update sub-shapes
605 TDF_LabelSequence aSubshapes;
606 aShapeTool->GetSubShapes( aLabel, aSubshapes );
607 for( TDF_LabelSequence::Iterator anItSs( aSubshapes ); anItSs.More(); anItSs.Next() )
608 {
609 const TDF_Label& aLSs = anItSs.Value();
610 const TopoDS_Shape aSs = aShapeTool->GetShape( aLSs );
611 const TopoDS_Shape aSs1 = aBRepTrsf.ModifiedShape( aSs );
612 aShapeTool->SetShape( aLSs, aSs1 );
613 }
614
615 // These attributes will be recomputed eventually, but clear them just in case
616 aLabel.ForgetAttribute( XCAFDoc_Area::GetID() );
617 aLabel.ForgetAttribute( XCAFDoc_Centroid::GetID() );
618 aLabel.ForgetAttribute( XCAFDoc_Volume::GetID() );
619 }
620 else if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence )
621 {
622 TopLoc_Location aLoc = aShapeTool->GetLocation( aLabel );
623 gp_Trsf aTrsf = aLoc.Transformation();
624 aTrsf.SetTranslationPart( aTrsf.TranslationPart().Multiplied( aScale ) );
625 XCAFDoc_Location::Set( aLabel, aTrsf );
626 }
627 }
628
629 if( !anIsDone )
630 {
631 return Standard_False;
632 }
633
634 aShapeTool->UpdateAssemblies();
635
636 return anIsDone;
637}
638
639
640static bool fuseShapes( auto& aInputShapes, TopoDS_Shape& aOutShape, REPORTER* aReporter )
641{
642 BRepAlgoAPI_Fuse mkFuse;
643 TopTools_ListOfShape shapeArguments, shapeTools;
644
645 for( TopoDS_Shape& sh : aInputShapes )
646 {
647 if( sh.IsNull() )
648 continue;
649
650 if( shapeArguments.IsEmpty() )
651 shapeArguments.Append( sh );
652 else
653 shapeTools.Append( sh );
654 }
655
656 mkFuse.SetRunParallel( true );
657 mkFuse.SetToFillHistory( false );
658 mkFuse.SetArguments( shapeArguments );
659 mkFuse.SetTools( shapeTools );
660 mkFuse.Build();
661
662 if( mkFuse.HasErrors() || mkFuse.HasWarnings() )
663 {
664 aReporter->Report( _( "** Got problems while fusing shapes **" ), RPT_SEVERITY_ERROR );
665
666 if( mkFuse.HasErrors() )
667 {
668 wxString msg = _( "Errors:\n" );
669 wxStringOutputStream os_stream( &msg );
670 wxStdOutputStream out( os_stream );
671
672 mkFuse.DumpErrors( out );
673 aReporter->Report( msg, RPT_SEVERITY_ERROR );
674 }
675
676 if( mkFuse.HasWarnings() )
677 {
678 wxString msg = _( "Warnings:\n" );
679 wxStringOutputStream os_stream( &msg );
680 wxStdOutputStream out( os_stream );
681
682 mkFuse.DumpWarnings( out );
683 aReporter->Report( msg, RPT_SEVERITY_WARNING );
684 }
685 }
686
687 if( mkFuse.IsDone() )
688 {
689 TopoDS_Shape fusedShape = mkFuse.Shape();
690
691 ShapeUpgrade_UnifySameDomain unify( fusedShape, true, true, false );
692 unify.History() = nullptr;
693 unify.Build();
694
695 TopoDS_Shape unifiedShapes = unify.Shape();
696
697 if( unifiedShapes.IsNull() )
698 {
699 aReporter->Report( _( "** ShapeUpgrade_UnifySameDomain produced a null shape **" ),
701 }
702 else
703 {
704 aOutShape = unifiedShapes;
705 return true;
706 }
707 }
708
709 return false;
710}
711
712
713static TopoDS_Compound makeCompound( const auto& aInputShapes )
714{
715 TopoDS_Compound compound;
716 BRep_Builder builder;
717 builder.MakeCompound( compound );
718
719 for( const TopoDS_Shape& shape : aInputShapes )
720 builder.Add( compound, shape );
721
722 return compound;
723}
724
725
726// Try to fuse shapes. If that fails, just add them to a compound
727static TopoDS_Shape fuseShapesOrCompound( const TopTools_ListOfShape& aInputShapes,
728 REPORTER* aReporter )
729{
730 TopoDS_Shape outShape;
731
732 if( aInputShapes.Size() == 1 )
733 return aInputShapes.First();
734
735 if( fuseShapes( aInputShapes, outShape, aReporter ) )
736 return outShape;
737
738 return makeCompound( aInputShapes );
739}
740
741
742// Sets names in assembly to <aPrefix> (<old name>), or to <aPrefix>
743static Standard_Boolean prefixNames( const TDF_Label& aLabel,
744 const TCollection_ExtendedString& aPrefix )
745{
746 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( aLabel );
747
748 if( aG.IsNull() )
749 {
750 Message::SendFail( "Couldn't create assembly graph." );
751 return Standard_False;
752 }
753
754 Standard_Boolean anIsDone = Standard_True;
755
756 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
757 {
758 const TDF_Label& lbl = aG->GetNode( idx );
759 Handle( TDataStd_Name ) nameHandle;
760
761 if( lbl.FindAttribute( TDataStd_Name::GetID(), nameHandle ) )
762 {
763 TCollection_ExtendedString name;
764
765 name += aPrefix;
766 name += " (";
767 name += nameHandle->Get();
768 name += ")";
769
770 TDataStd_Name::Set( lbl, name );
771 }
772 else
773 {
774 TDataStd_Name::Set( lbl, aPrefix );
775 }
776 }
777
778 return anIsDone;
779}
780
781
782STEP_PCB_MODEL::STEP_PCB_MODEL( const wxString& aPcbName, REPORTER* aReporter ) :
783 m_reporter( aReporter )
784{
785 m_app = XCAFApp_Application::GetApplication();
786 m_app->NewDocument( "MDTV-XCAF", m_doc );
787 m_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
788 m_assy_label = m_assy->NewShape();
789 m_hasPCB = false;
790 m_simplifyShapes = true;
791 m_components = 0;
795 m_minx = 1.0e10; // absurdly large number; any valid PCB X value will be smaller
796 m_pcbName = aPcbName;
797 m_fuseShapes = false;
798 m_extraPadThickness = true;
800}
801
802
804{
805 if( m_doc->CanClose() == CDM_CCS_OK )
806 m_doc->Close();
807}
808
809
810bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia,
811 SHAPE_POLY_SET* aClipPolygon )
812{
813 const double c_padExtraThickness = 0.005;
814 bool success = true;
815 std::vector<TopoDS_Shape> padShapes;
816 bool castellated = aClipPolygon && aPad->GetProperty() == PAD_PROP::CASTELLATED;
817
818 for( PCB_LAYER_ID pcb_layer : aPad->GetLayerSet().Seq() )
819 {
820 if( !m_enabledLayers.Contains( pcb_layer ) )
821 continue;
822
823 if( pcb_layer == F_Mask || pcb_layer == B_Mask )
824 continue;
825
826 if( !aPad->FlashLayer( pcb_layer ) )
827 continue;
828
829 double Zpos, thickness;
830 getLayerZPlacement( pcb_layer, Zpos, thickness );
831
832 if( !aVia && m_extraPadThickness )
833 {
834 // Pad surface as a separate face for FEM simulations.
835 if( pcb_layer == F_Cu )
836 thickness += c_padExtraThickness;
837 else if( pcb_layer == B_Cu )
838 thickness -= c_padExtraThickness;
839 }
840
841 TopoDS_Shape testShape;
842
843 // Make a shape on copper layers
844 SHAPE_POLY_SET polySet;
845 aPad->TransformShapeToPolygon( polySet, pcb_layer, 0, aPad->GetMaxError(), ERROR_INSIDE );
846
847 if( castellated )
848 {
849 polySet.ClearArcs();
850 polySet.BooleanIntersection( *aClipPolygon );
851 }
852
853 success &= MakeShapes( padShapes, polySet, m_simplifyShapes, thickness, Zpos, aOrigin );
854
855 if( testShape.IsNull() )
856 {
857 std::vector<TopoDS_Shape> testShapes;
858
859 MakeShapes( testShapes, polySet, m_simplifyShapes, 0.0, Zpos + thickness, aOrigin );
860
861 if( testShapes.size() > 0 )
862 testShape = testShapes.front();
863 }
864
865 if( !aVia && m_extraPadThickness && !testShape.IsNull() )
866 {
867 if( pcb_layer == F_Cu || pcb_layer == B_Cu )
868 {
869 wxString name;
870
871 name << "Pad_";
872
873 if( pcb_layer == F_Cu )
874 name << 'F' << '_';
875 else if( pcb_layer == B_Cu )
876 name << 'B' << '_';
877
878 name << aPad->GetParentFootprint()->GetReferenceAsString() << '_'
879 << aPad->GetNumber() << '_' << aPad->GetShortNetname();
880
881 gp_Pnt point( pcbIUScale.IUTomm( aPad->GetX() - aOrigin.x ),
882 -pcbIUScale.IUTomm( aPad->GetY() - aOrigin.y ), Zpos + thickness );
883
884 m_pad_points[name].emplace_back( point, testShape );
885 }
886 }
887 }
888
889 if( aPad->GetAttribute() == PAD_ATTRIB::PTH && aPad->IsOnLayer( F_Cu ) && aPad->IsOnLayer( B_Cu ) )
890 {
891 double f_pos, f_thickness;
892 double b_pos, b_thickness;
893 getLayerZPlacement( F_Cu, f_pos, f_thickness );
894 getLayerZPlacement( B_Cu, b_pos, b_thickness );
895
896 if( !aVia && m_extraPadThickness )
897 {
898 // Pad surface is slightly thicker
899 f_thickness += c_padExtraThickness;
900 b_thickness -= c_padExtraThickness;
901 }
902
903 double top = std::max( f_pos, f_pos + f_thickness );
904 double bottom = std::min( b_pos, b_pos + b_thickness );
905 double hole_height = top - bottom;
906
907 TopoDS_Shape plating;
908
909 std::shared_ptr<SHAPE_SEGMENT> seg_hole = aPad->GetEffectiveHoleShape();
910 double width = std::min( aPad->GetDrillSize().x, aPad->GetDrillSize().y );
911
912 if( !castellated )
913 {
914 if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
915 hole_height, bottom, aOrigin ) )
916 {
917 padShapes.push_back( plating );
918 }
919 else
920 {
921 success = false;
922 }
923 }
924 else
925 {
926 // Note:
927 // the truncated hole shape is exported as a vertical filled shape. The hole itself
928 // will be removed later, when all holes are removed from the board
929 SHAPE_POLY_SET polyHole;
930
931 if( seg_hole->GetSeg().A == seg_hole->GetSeg().B ) // Hole is a circle
932 {
933 TransformCircleToPolygon( polyHole, seg_hole->GetSeg().A, width/2,
934 aPad->GetMaxError(), ERROR_OUTSIDE );
935
936 }
937 else
938 {
939 TransformOvalToPolygon( polyHole, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
940 aPad->GetMaxError(), ERROR_OUTSIDE );
941 }
942
943 polyHole.ClearArcs();
944 polyHole.BooleanIntersection( *aClipPolygon );
945
946 if( MakePolygonAsWall( plating, polyHole, hole_height, bottom, aOrigin ) )
947 {
948 padShapes.push_back( plating );
949 }
950 else
951 {
952 success = false;
953 }
954 }
955 }
956
957 if( !success ) // Error
958 m_reporter->Report( _( "OCC error adding pad/via polygon." ), RPT_SEVERITY_ERROR );
959
960 if( !padShapes.empty() )
961 {
962 // Fuse pad shapes here before fusing them with tracks because OCCT sometimes has trouble
963 if( m_fuseShapes )
964 {
965 TopTools_ListOfShape padShapesList;
966
967 for( const TopoDS_Shape& shape : padShapes )
968 padShapesList.Append( shape );
969
970 m_board_copper_pads[aPad->GetNetname()].push_back( fuseShapesOrCompound( padShapesList, m_reporter ) );
971 }
972 else
973 {
974 for( const TopoDS_Shape& shape : padShapes )
975 m_board_copper_pads[aPad->GetNetname()].push_back( shape );
976 }
977 }
978
979 return success;
980}
981
982
983bool STEP_PCB_MODEL::AddHole( const SHAPE_SEGMENT& aShape, int aPlatingThickness,
984 PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia,
985 const VECTOR2D& aOrigin, bool aCutCopper, bool aCutBody )
986{
987 double margin = 0.001; // a small margin on the Z axix to be sure the hole
988 // is bigger than the board with copper
989 // must be > OCC_MAX_DISTANCE_TO_MERGE_POINTS
990
991 // Pads are taller by 0.01 mm
992 if( !aVia && m_extraPadThickness)
993 margin += 0.01;
994
995 double f_pos, f_thickness;
996 double b_pos, b_thickness;
997 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
998 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
999 double top = std::max( f_pos, f_pos + f_thickness );
1000 double bottom = std::min( b_pos, b_pos + b_thickness );
1001
1002 double holeZsize = ( top - bottom ) + ( margin * 2 );
1003
1004 double boardDrill = aShape.GetWidth();
1005 double copperDrill = boardDrill - aPlatingThickness * 2;
1006
1007 TopoDS_Shape copperHole, boardHole;
1008
1009 if( aCutCopper )
1010 {
1011 if( MakeShapeAsThickSegment( copperHole, aShape.GetSeg().A, aShape.GetSeg().B, copperDrill,
1012 holeZsize, bottom - margin, aOrigin ) )
1013 {
1014 m_copperCutouts.push_back( copperHole );
1015 }
1016 else
1017 {
1018 return false;
1019 }
1020 }
1021
1022 if( aCutBody )
1023 {
1024 if( MakeShapeAsThickSegment( boardHole, aShape.GetSeg().A, aShape.GetSeg().B, boardDrill,
1025 holeZsize, bottom - margin, aOrigin ) )
1026 {
1027 m_boardCutouts.push_back( boardHole );
1028 }
1029 else
1030 {
1031 return false;
1032 }
1033 }
1034
1035 return true;
1036}
1037
1038
1040 PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D& aOrigin,
1041 const wxString& aNetname )
1042{
1043 double f_pos, f_thickness;
1044 double b_pos, b_thickness;
1045 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
1046 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
1047 double top = std::max( f_pos, f_pos + f_thickness );
1048 double bottom = std::min( b_pos, b_pos + b_thickness );
1049
1050 TopoDS_Shape plating;
1051
1052 if( !MakeShapeAsThickSegment( plating, aShape.GetSeg().A, aShape.GetSeg().B, aShape.GetWidth(),
1053 ( top - bottom ), bottom, aOrigin ) )
1054 {
1055 return false;
1056 }
1057
1058 if( aVia )
1059 m_board_copper_vias[aNetname].push_back( plating );
1060 else
1061 m_board_copper_pads[aNetname].push_back( plating );
1062
1063 return true;
1064}
1065
1066
1068 PCB_LAYER_ID aLayerEnd, const VECTOR2D& aOrigin )
1069{
1070 // A backdrill removes board material and copper plating between two layers.
1071 // The backdrill typically starts from an outer layer and drills into an inner layer.
1072 // For example, a top backdrill starts at F_Cu and ends at an inner layer.
1073 // A bottom backdrill starts at B_Cu and ends at an inner layer.
1074
1075 double margin = 0.001; // a small margin on the Z axis to ensure the hole
1076 // is bigger than the board section being removed
1077
1078 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1079 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1080
1081 double start_pos, start_thickness;
1082 double end_pos, end_thickness;
1083 getLayerZPlacement( aLayerStart, start_pos, start_thickness );
1084 getLayerZPlacement( aLayerEnd, end_pos, end_thickness );
1085
1086 // Calculate the Z extent of the backdrill
1087 double top = std::max( { start_pos, start_pos + start_thickness,
1088 end_pos, end_pos + end_thickness } );
1089 double bottom = std::min( { start_pos, start_pos + start_thickness,
1090 end_pos, end_pos + end_thickness } );
1091
1092 // Extend past outer copper layers if the backdrill reaches them
1093 if( aLayerStart == F_Cu || aLayerEnd == F_Cu )
1094 top += copperMargin;
1095 if( aLayerStart == B_Cu || aLayerEnd == B_Cu )
1096 bottom -= copperMargin;
1097
1098 double holeZsize = ( top - bottom ) + ( margin * 2 );
1099 double holeZpos = bottom - margin;
1100
1101 double backdrillDiameter = aShape.GetWidth();
1102
1103 TopoDS_Shape backdrillHole;
1104
1105 // Create the backdrill hole shape - this cuts the board body
1106 if( MakeShapeAsThickSegment( backdrillHole, aShape.GetSeg().A, aShape.GetSeg().B,
1107 backdrillDiameter, holeZsize, holeZpos, aOrigin ) )
1108 {
1109 m_boardCutouts.push_back( backdrillHole );
1110
1111 // This removes annular rings and barrel copper between the backdrill layers.
1112 m_copperCutouts.push_back( backdrillHole );
1113 }
1114 else
1115 {
1116 return false;
1117 }
1118
1119 return true;
1120}
1121
1122
1123bool STEP_PCB_MODEL::AddCounterbore( const VECTOR2I& aPosition, int aDiameter, int aDepth,
1124 bool aFrontSide, const VECTOR2D& aOrigin )
1125{
1126 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: pos=(%d,%d) diameter=%d depth=%d frontSide=%d origin=(%f,%f)" ),
1127 aPosition.x, aPosition.y, aDiameter, aDepth, aFrontSide ? 1 : 0, aOrigin.x, aOrigin.y );
1128
1129 // A counterbore is a cylindrical recess from the top or bottom of the board
1130 if( aDiameter <= 0 || aDepth <= 0 )
1131 {
1132 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: REJECTED - invalid diameter=%d or depth=%d" ),
1133 aDiameter, aDepth );
1134 return false;
1135 }
1136
1137 double margin = 0.001; // small margin to ensure clean cuts
1138
1139 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1140 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1141
1142 // Get board body position (between copper layers)
1143 double boardZpos, boardThickness;
1144 getBoardBodyZPlacement( boardZpos, boardThickness );
1145
1146 // Get copper layer positions - these extend beyond the board body
1147 double f_pos, f_thickness, b_pos, b_thickness;
1148 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1149 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1150
1151 // Calculate actual outer surfaces including copper
1152 // F_Cu: f_pos is inner surface, f_pos + f_thickness is outer surface (copper extends upward)
1153 // B_Cu: b_pos is inner surface, b_pos + b_thickness is outer surface (thickness is negative, copper extends downward)
1154 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1155 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1156
1157 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: boardZpos=%f boardThickness=%f f_pos=%f f_thickness=%f topOuter=%f bottomOuter=%f" ),
1158 boardZpos, boardThickness, f_pos, f_thickness, topOuterSurface, bottomOuterSurface );
1159
1160 // Convert dimensions to mm
1161 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1162 double depth_mm = pcbIUScale.IUTomm( aDepth );
1163 double radius_mm = diameter_mm / 2.0;
1164
1165 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: diameter_mm=%f depth_mm=%f radius_mm=%f" ),
1166 diameter_mm, depth_mm, radius_mm );
1167
1168 // Calculate cylinder position based on which side
1169 // The cylinder must extend past the outer surface to ensure complete copper removal
1170 double cylinderZpos;
1171 double cylinderHeight;
1172
1173 if( aFrontSide )
1174 {
1175 // Counterbore from top - cylinder extends from above outer copper surface down to depth
1176 // Add copperMargin above the surface to ensure complete annular ring removal
1177 cylinderZpos = topOuterSurface - depth_mm - margin;
1178 cylinderHeight = depth_mm + copperMargin + 2 * margin;
1179 }
1180 else
1181 {
1182 // Counterbore from bottom - cylinder extends from below outer copper surface up to depth
1183 // Add copperMargin below the surface to ensure complete annular ring removal
1184 cylinderZpos = bottomOuterSurface - copperMargin - margin;
1185 cylinderHeight = depth_mm + copperMargin + 2 * margin;
1186 }
1187
1188 // Convert position to mm
1189 double posX_mm = pcbIUScale.IUTomm( aPosition.x - aOrigin.x );
1190 double posY_mm = -pcbIUScale.IUTomm( aPosition.y - aOrigin.y );
1191
1192 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: posX_mm=%f posY_mm=%f cylinderZpos=%f cylinderHeight=%f" ),
1193 posX_mm, posY_mm, cylinderZpos, cylinderHeight );
1194
1195 try
1196 {
1197 // Create coordinate system for the cylinder
1198 // The cylinder axis is along Z, positioned at the counterbore center
1199 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, cylinderZpos ), gp::DZ() );
1200
1201 TopoDS_Shape cylinder = BRepPrimAPI_MakeCylinder( axis, radius_mm, cylinderHeight );
1202
1203 if( cylinder.IsNull() )
1204 {
1205 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: FAILED - cylinder shape is null" ) );
1206 m_reporter->Report( _( "Failed to create counterbore cylinder shape" ),
1208 return false;
1209 }
1210
1211 // Add to both board and copper cutouts
1212 m_boardCutouts.push_back( cylinder );
1213 m_copperCutouts.push_back( cylinder );
1214
1215 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: SUCCESS - added cylinder. boardCutouts=%zu copperCutouts=%zu" ),
1216 m_boardCutouts.size(), m_copperCutouts.size() );
1217 }
1218 catch( const Standard_Failure& e )
1219 {
1220 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: EXCEPTION - %s" ), e.GetMessageString() );
1221 m_reporter->Report( wxString::Format( _( "OCC exception creating counterbore: %s" ),
1222 e.GetMessageString() ),
1224 return false;
1225 }
1226
1227 return true;
1228}
1229
1230
1231bool STEP_PCB_MODEL::AddCountersink( const VECTOR2I& aPosition, int aDiameter, int aDepth,
1232 int aAngle, bool aFrontSide, const VECTOR2D& aOrigin )
1233{
1234 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: pos=(%d,%d) diameter=%d depth=%d angle=%d frontSide=%d origin=(%f,%f)" ),
1235 aPosition.x, aPosition.y, aDiameter, aDepth, aAngle, aFrontSide ? 1 : 0, aOrigin.x, aOrigin.y );
1236
1237 // A countersink is a conical recess from the top or bottom of the board
1238 // The angle parameter is the total cone angle in decidegrees
1239 // (angle between opposite sides of the cone)
1240 if( aDiameter <= 0 || aAngle <= 0 )
1241 {
1242 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: REJECTED - invalid diameter=%d or angle=%d" ),
1243 aDiameter, aAngle );
1244 return false;
1245 }
1246
1247 double margin = 0.001; // small margin to ensure clean cuts
1248
1249 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1250 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1251
1252 // Get board body position (between copper layers)
1253 double boardZpos, boardThickness;
1254 getBoardBodyZPlacement( boardZpos, boardThickness );
1255
1256 // Get copper layer positions - these extend beyond the board body
1257 double f_pos, f_thickness, b_pos, b_thickness;
1258 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1259 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1260
1261 // Calculate actual outer surfaces including copper
1262 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1263 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1264
1265 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: boardZpos=%f boardThickness=%f f_pos=%f f_thickness=%f topOuter=%f bottomOuter=%f" ),
1266 boardZpos, boardThickness, f_pos, f_thickness, topOuterSurface, bottomOuterSurface );
1267
1268 // Convert dimensions to mm
1269 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1270 double radius_mm = diameter_mm / 2.0;
1271
1272 // Convert angle from decidegrees to radians
1273 // aAngle is the total cone angle, so half-angle is used for geometry
1274 double halfAngleRad = ( aAngle / 10.0 ) * M_PI / 180.0 / 2.0;
1275
1276 // If depth is not specified, calculate it from the diameter and angle
1277 // The countersink depth is the full cone height: depth = radius / tan(halfAngle)
1278 double depth_mm;
1279 if( aDepth <= 0 )
1280 {
1281 // Calculate depth from diameter and angle
1282 depth_mm = radius_mm / tan( halfAngleRad );
1283 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: depth not specified, calculated depth_mm=%f from radius=%f and angle" ),
1284 depth_mm, radius_mm );
1285 }
1286 else
1287 {
1288 depth_mm = pcbIUScale.IUTomm( aDepth );
1289 }
1290
1291 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: diameter_mm=%f depth_mm=%f radius_mm=%f halfAngleRad=%f (deg=%f)" ),
1292 diameter_mm, depth_mm, radius_mm, halfAngleRad, halfAngleRad * 180.0 / M_PI );
1293
1294 // Calculate the cone geometry
1295 // For a countersink, R1 (bottom radius) may be 0 (sharp point) or non-zero
1296 // R2 (top radius) is at the surface
1297 // The cone depth determines how deep it goes
1298
1299 // Calculate bottom radius based on depth and angle
1300 // tan(halfAngle) = (R2 - R1) / depth
1301 // If we want the surface radius to be radius_mm and depth to be depth_mm:
1302 // R1 = R2 - depth * tan(halfAngle)
1303 double bottomRadius_mm = radius_mm - depth_mm * tan( halfAngleRad );
1304
1305 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: bottomRadius_mm=%f (before clamp), tan(halfAngle)=%f" ),
1306 bottomRadius_mm, tan( halfAngleRad ) );
1307
1308 if( bottomRadius_mm < 0 )
1309 bottomRadius_mm = 0; // Cone comes to a point before reaching full depth
1310
1311 // Calculate position based on which side
1312 // Extend the cone past the outer surface by copperMargin to ensure complete copper removal
1313 double coneZpos;
1314 double coneHeight = depth_mm + copperMargin + margin;
1315 double r1, r2; // bottom and top radii for BRepPrimAPI_MakeCone
1316
1317 // Convert position to mm
1318 double posX_mm = pcbIUScale.IUTomm( aPosition.x - aOrigin.x );
1319 double posY_mm = -pcbIUScale.IUTomm( aPosition.y - aOrigin.y );
1320
1321 try
1322 {
1323 TopoDS_Shape cone;
1324
1325 if( aFrontSide )
1326 {
1327 // Countersink from top - cone apex points down
1328 // In OCC, cone is built from z=0 to z=H with R1 at z=0 and R2 at z=H
1329 // For a top countersink, we want large radius at top, small at bottom
1330 coneZpos = topOuterSurface - depth_mm - margin;
1331 r1 = bottomRadius_mm; // smaller radius at bottom (deeper into board)
1332 // Extend the top radius to account for the copperMargin extension above the surface
1333 r2 = radius_mm + ( copperMargin + margin ) * tan( halfAngleRad );
1334
1335 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: FRONT - coneZpos=%f r1=%f r2=%f coneHeight=%f" ),
1336 coneZpos, r1, r2, coneHeight );
1337
1338 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, coneZpos ), gp::DZ() );
1339 cone = BRepPrimAPI_MakeCone( axis, r1, r2, coneHeight );
1340 }
1341 else
1342 {
1343 // Countersink from bottom - cone apex points up
1344 // For bottom countersink, large radius at bottom, small at top
1345 // Extend below the surface by copperMargin
1346 coneZpos = bottomOuterSurface - copperMargin - margin;
1347 // Extend the bottom radius to account for the copperMargin extension below the surface
1348 r1 = radius_mm + ( copperMargin + margin ) * tan( halfAngleRad );
1349 r2 = bottomRadius_mm; // smaller radius at top (deeper into board)
1350
1351 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: BACK - coneZpos=%f r1=%f r2=%f coneHeight=%f" ),
1352 coneZpos, r1, r2, coneHeight );
1353
1354 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, coneZpos ), gp::DZ() );
1355 cone = BRepPrimAPI_MakeCone( axis, r1, r2, coneHeight );
1356 }
1357
1358 if( cone.IsNull() )
1359 {
1360 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: FAILED - cone shape is null" ) );
1361 m_reporter->Report( _( "Failed to create countersink cone shape" ),
1363 return false;
1364 }
1365
1366 // Add to both board and copper cutouts
1367 m_boardCutouts.push_back( cone );
1368 m_copperCutouts.push_back( cone );
1369
1370 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: SUCCESS - added cone. boardCutouts=%zu copperCutouts=%zu" ),
1371 m_boardCutouts.size(), m_copperCutouts.size() );
1372 }
1373 catch( const Standard_Failure& e )
1374 {
1375 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: EXCEPTION - %s" ), e.GetMessageString() );
1376 m_reporter->Report( wxString::Format( _( "OCC exception creating countersink: %s" ),
1377 e.GetMessageString() ),
1379 return false;
1380 }
1381
1382 return true;
1383}
1384
1385
1386std::map<PCB_LAYER_ID, int> STEP_PCB_MODEL::GetCopperLayerKnockouts( int aDiameter, int aDepth,
1387 int aAngle, bool aFrontSide )
1388{
1389 std::map<PCB_LAYER_ID, int> knockouts;
1390
1391 // Get the outer surface positions (including copper)
1392 double f_pos, f_thickness, b_pos, b_thickness;
1393 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1394 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1395
1396 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1397 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1398
1399 // Convert dimensions to mm
1400 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1401 double radius_mm = diameter_mm / 2.0;
1402
1403 // Calculate depth in mm
1404 double depth_mm;
1405 double halfAngleRad = 0.0;
1406
1407 if( aAngle > 0 )
1408 {
1409 // Countersink - calculate half angle
1410 halfAngleRad = ( aAngle / 10.0 ) * M_PI / 180.0 / 2.0;
1411
1412 // If depth is not specified for countersink, calculate from diameter and angle
1413 if( aDepth <= 0 )
1414 depth_mm = radius_mm / tan( halfAngleRad );
1415 else
1416 depth_mm = pcbIUScale.IUTomm( aDepth );
1417 }
1418 else
1419 {
1420 // Counterbore - use specified depth
1421 depth_mm = pcbIUScale.IUTomm( aDepth );
1422 }
1423
1424 // Determine the Z range of the feature
1425 double featureTop, featureBottom;
1426
1427 if( aFrontSide )
1428 {
1429 featureTop = topOuterSurface;
1430 featureBottom = topOuterSurface - depth_mm;
1431 }
1432 else
1433 {
1434 featureBottom = bottomOuterSurface;
1435 featureTop = bottomOuterSurface + depth_mm;
1436 }
1437
1438 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: featureTop=%f featureBottom=%f depth_mm=%f frontSide=%d" ),
1439 featureTop, featureBottom, depth_mm, aFrontSide ? 1 : 0 );
1440
1441 // Iterate through all copper layers and check if they fall within the feature range
1442 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
1443 {
1444 if( item->GetType() != BS_ITEM_TYPE_COPPER )
1445 continue;
1446
1447 PCB_LAYER_ID layer = item->GetBrdLayerId();
1448 double layerZ, layerThickness;
1449 getLayerZPlacement( layer, layerZ, layerThickness );
1450
1451 // Get the Z range of this copper layer (both inner and outer surfaces)
1452 double layerTop = std::max( layerZ, layerZ + layerThickness );
1453 double layerBottom = std::min( layerZ, layerZ + layerThickness );
1454
1455 // Check if this layer overlaps with the feature Z range
1456 // A layer is affected if any part of it is within the feature range
1457 bool layerInRange = ( layerTop >= featureBottom && layerBottom <= featureTop );
1458
1459 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d Z=[%f, %f] feature=[%f, %f] inRange=%d" ),
1460 static_cast<int>( layer ), layerBottom, layerTop, featureBottom, featureTop, layerInRange ? 1 : 0 );
1461
1462 if( !layerInRange )
1463 continue;
1464
1465 int knockoutDiameter;
1466
1467 if( aAngle > 0 )
1468 {
1469 // Countersink - calculate diameter at this layer's Z level
1470 // Use the layer surface that's closest to the feature origin surface
1471 double layerSurfaceZ;
1472 if( aFrontSide )
1473 {
1474 // For front-side countersink, use the top surface of the layer
1475 layerSurfaceZ = layerTop;
1476 }
1477 else
1478 {
1479 // For back-side countersink, use the bottom surface of the layer
1480 layerSurfaceZ = layerBottom;
1481 }
1482
1483 // Distance from the surface determines the radius at this Z
1484 double distanceFromSurface;
1485 if( aFrontSide )
1486 distanceFromSurface = topOuterSurface - layerSurfaceZ;
1487 else
1488 distanceFromSurface = layerSurfaceZ - bottomOuterSurface;
1489
1490 // Radius at this depth: r = R - d * tan(halfAngle)
1491 double radiusAtLayer_mm = radius_mm - distanceFromSurface * tan( halfAngleRad );
1492
1493 if( radiusAtLayer_mm <= 0 )
1494 {
1495 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d - countersink tapers to point before this layer" ),
1496 static_cast<int>( layer ) );
1497 continue; // Cone tapers to a point before reaching this layer
1498 }
1499
1500 knockoutDiameter = pcbIUScale.mmToIU( radiusAtLayer_mm * 2.0 );
1501 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d (countersink) - distFromSurface=%f radiusAtLayer=%f diameter=%d" ),
1502 static_cast<int>( layer ), distanceFromSurface, radiusAtLayer_mm, knockoutDiameter );
1503 }
1504 else
1505 {
1506 // Counterbore - constant diameter
1507 knockoutDiameter = aDiameter;
1508 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d (counterbore) - diameter=%d" ),
1509 static_cast<int>( layer ), knockoutDiameter );
1510 }
1511
1512 knockouts[layer] = knockoutDiameter;
1513 }
1514
1515 return knockouts;
1516}
1517
1518
1519void STEP_PCB_MODEL::getLayerZPlacement( const PCB_LAYER_ID aLayer, double& aZPos,
1520 double& aThickness )
1521{
1522 // Offsets above copper in mm
1523 static const double c_silkscreenAboveCopper = 0.04;
1524 static const double c_soldermaskAboveCopper = 0.015;
1525
1526 if( IsCopperLayer( aLayer ) )
1527 {
1528 getCopperLayerZPlacement( aLayer, aZPos, aThickness );
1529 }
1530 else if( IsFrontLayer( aLayer ) )
1531 {
1532 double f_pos, f_thickness;
1533 getCopperLayerZPlacement( F_Cu, f_pos, f_thickness );
1534 double top = std::max( f_pos, f_pos + f_thickness );
1535
1536 if( aLayer == F_SilkS )
1537 aZPos = top + c_silkscreenAboveCopper;
1538 else
1539 aZPos = top + c_soldermaskAboveCopper;
1540
1541 aThickness = 0.0; // Normal points up
1542 }
1543 else if( IsBackLayer( aLayer ) )
1544 {
1545 double b_pos, b_thickness;
1546 getCopperLayerZPlacement( B_Cu, b_pos, b_thickness );
1547 double bottom = std::min( b_pos, b_pos + b_thickness );
1548
1549 if( aLayer == B_SilkS )
1550 aZPos = bottom - c_silkscreenAboveCopper;
1551 else
1552 aZPos = bottom - c_soldermaskAboveCopper;
1553
1554 aThickness = -0.0; // Normal points down
1555 }
1556}
1557
1558
1560 double& aThickness )
1561{
1562 int z = 0;
1563 int thickness = 0;
1564 bool wasPrepreg = false;
1565
1566 const std::vector<BOARD_STACKUP_ITEM*>& materials = m_stackup.GetList();
1567
1568 // Iterate from bottom to top
1569 for( auto it = materials.rbegin(); it != materials.rend(); ++it )
1570 {
1571 const BOARD_STACKUP_ITEM* item = *it;
1572
1573 if( item->GetType() == BS_ITEM_TYPE_COPPER )
1574 {
1575 if( aLayer == B_Cu )
1576 {
1577 // This is the first encountered layer
1578 thickness = -item->GetThickness();
1579 break;
1580 }
1581
1582 // Inner copper position is usually inside prepreg
1583 if( wasPrepreg && item->GetBrdLayerId() != F_Cu )
1584 {
1585 z += item->GetThickness();
1586 thickness = -item->GetThickness();
1587 }
1588 else
1589 {
1590 thickness = item->GetThickness();
1591 }
1592
1593 if( item->GetBrdLayerId() == aLayer )
1594 break;
1595
1596 if( !wasPrepreg && item->GetBrdLayerId() != B_Cu )
1597 z += item->GetThickness();
1598 }
1599 else if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
1600 {
1601 wasPrepreg = ( item->GetTypeName() == KEY_PREPREG );
1602
1603 // Dielectric can have sub-layers. Layer 0 is the main layer
1604 // Not frequent, but possible
1605 thickness = 0;
1606 for( int idx = 0; idx < item->GetSublayersCount(); idx++ )
1607 thickness += item->GetThickness( idx );
1608
1609 z += thickness;
1610 }
1611 }
1612
1613 aZPos = pcbIUScale.IUTomm( z );
1614 aThickness = pcbIUScale.IUTomm( thickness );
1615}
1616
1617
1618void STEP_PCB_MODEL::getBoardBodyZPlacement( double& aZPos, double& aThickness )
1619{
1620 double f_pos, f_thickness;
1621 double b_pos, b_thickness;
1622 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1623 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1624 double top = std::min( f_pos, f_pos + f_thickness );
1625 double bottom = std::max( b_pos, b_pos + b_thickness );
1626
1627 aThickness = ( top - bottom );
1628 aZPos = bottom;
1629
1630 wxASSERT( aZPos == 0.0 );
1631}
1632
1633
1635 const VECTOR2D& aOrigin, const wxString& aNetname )
1636{
1637 bool success = true;
1638
1639 if( aPolyShapes->IsEmpty() )
1640 return true;
1641
1642 if( !m_enabledLayers.Contains( aLayer ) )
1643 return true;
1644
1645 double z_pos, thickness;
1646 getLayerZPlacement( aLayer, z_pos, thickness );
1647
1648 std::vector<TopoDS_Shape>* targetVec = nullptr;
1649
1650 if( IsCopperLayer( aLayer ) )
1651 targetVec = &m_board_copper[aNetname];
1652 else if( aLayer == F_SilkS )
1653 targetVec = &m_board_front_silk;
1654 else if( aLayer == B_SilkS )
1655 targetVec = &m_board_back_silk;
1656 else if( aLayer == F_Mask )
1657 targetVec = &m_board_front_mask;
1658 else
1659 targetVec = &m_board_back_mask;
1660
1661 if( !MakeShapes( *targetVec, *aPolyShapes, m_simplifyShapes, thickness, z_pos, aOrigin ) )
1662 {
1663 m_reporter->Report( wxString::Format( _( "Could not add shape (%d points) to copper layer %s." ),
1664 aPolyShapes->FullPointCount(),
1665 LayerName( aLayer ) ),
1667
1668 success = false;
1669 }
1670
1671 return success;
1672}
1673
1674
1675bool STEP_PCB_MODEL::AddComponent( const std::string& aFileNameUTF8, const std::string& aRefDes,
1676 bool aBottom, const VECTOR2D& aPosition, double aRotation, const VECTOR3D& aOffset,
1677 const VECTOR3D& aOrientation, const VECTOR3D& aScale, bool aSubstituteModels )
1678{
1679 if( aFileNameUTF8.empty() )
1680 {
1681 m_reporter->Report( wxString::Format( _( "No model defined for %s." ), aRefDes ),
1683 return false;
1684 }
1685
1686 wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
1687 m_reporter->Report( wxString::Format( wxT( "Adding component %s." ), aRefDes ), RPT_SEVERITY_DEBUG );
1688
1689 // first retrieve a label
1690 TDF_Label lmodel;
1691 wxString errorMessage;
1692
1693 if( !getModelLabel( aFileNameUTF8, aScale, lmodel, aSubstituteModels, &errorMessage ) )
1694 {
1695 if( errorMessage.IsEmpty() )
1696 errorMessage.Printf( _( "No model for filename '%s'." ), fileName );
1697
1698 m_reporter->Report( errorMessage, RPT_SEVERITY_ERROR );
1699 return false;
1700 }
1701
1702 // calculate the Location transform
1703 TopLoc_Location toploc;
1704
1705 if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
1706 {
1707 m_reporter->Report( wxString::Format( _( "No location data for filename '%s'." ), fileName ),
1709 return false;
1710 }
1711
1712 // add the located sub-assembly
1713 TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
1714
1715 if( llabel.IsNull() )
1716 {
1717 m_reporter->Report( wxString::Format( _( "Could not add component with filename '%s'." ), fileName ),
1719 return false;
1720 }
1721
1722 // attach the RefDes name
1723 TCollection_ExtendedString refdes( aRefDes.c_str() );
1724 TDataStd_Name::Set( llabel, refdes );
1725
1726 KICAD3D_INFO::Set( llabel, KICAD3D_MODEL_TYPE::COMPONENT, aRefDes );
1727
1728 return true;
1729}
1730
1731
1733{
1734 m_enabledLayers = aLayers;
1735}
1736
1737
1739{
1740 m_fuseShapes = aValue;
1741}
1742
1743
1745{
1746 m_simplifyShapes = aValue;
1747}
1748
1749
1751{
1752 m_stackup = aStackup;
1753}
1754
1755
1756void STEP_PCB_MODEL::SetNetFilter( const wxString& aFilter )
1757{
1758 m_netFilter = aFilter;
1759}
1760
1761
1763{
1764 m_extraPadThickness = aValue;
1765}
1766
1767
1768void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
1769{
1770 m_copperColor[0] = r;
1771 m_copperColor[1] = g;
1772 m_copperColor[2] = b;
1773}
1774
1775
1776void STEP_PCB_MODEL::SetPadColor( double r, double g, double b )
1777{
1778 m_padColor[0] = r;
1779 m_padColor[1] = g;
1780 m_padColor[2] = b;
1781}
1782
1783
1785{
1786 // Ensure a minimal value (in mm)
1787 m_mergeOCCMaxDist = aDistance;
1788}
1789
1790
1792{
1793 return m_pcb_labels.size() > 0;
1794}
1795
1796
1797bool STEP_PCB_MODEL::MakeShapeAsThickSegment( TopoDS_Shape& aShape, const VECTOR2D& aStartPoint,
1798 const VECTOR2D& aEndPoint, double aWidth, double aThickness,
1799 double aZposition, const VECTOR2D& aOrigin )
1800{
1801 // make a wide segment from 2 lines and 2 180 deg arcs
1802 // We need 6 points (3 per arcs)
1803 VECTOR2D coords[6];
1804
1805 // We build a horizontal segment, and after rotate it
1806 double len = ( aEndPoint - aStartPoint ).EuclideanNorm();
1807 double h_width = aWidth/2.0;
1808 // First is end point of first arc, and also start point of first line
1809 coords[0] = VECTOR2D{ 0.0, h_width };
1810
1811 // end point of first line and start point of second arc
1812 coords[1] = VECTOR2D{ len, h_width };
1813
1814 // middle point of second arc
1815 coords[2] = VECTOR2D{ len + h_width, 0.0 };
1816
1817 // start point of second line and end point of second arc
1818 coords[3] = VECTOR2D{ len, -h_width };
1819
1820 // end point of second line and start point of first arc
1821 coords[4] = VECTOR2D{ 0, -h_width };
1822
1823 // middle point of first arc
1824 coords[5] = VECTOR2D{ -h_width, 0.0 };
1825
1826 // Rotate and move to segment position
1827 EDA_ANGLE seg_angle( aEndPoint - aStartPoint );
1828
1829 for( int ii = 0; ii < 6; ii++ )
1830 {
1831 RotatePoint( coords[ii], VECTOR2D{ 0, 0 }, -seg_angle ),
1832 coords[ii] += aStartPoint;
1833 }
1834
1835
1836 // Convert to 3D points
1837 gp_Pnt coords3D[ 6 ];
1838
1839 for( int ii = 0; ii < 6; ii++ )
1840 {
1841 coords3D[ii] = gp_Pnt( pcbIUScale.IUTomm( coords[ii].x - aOrigin.x ),
1842 -pcbIUScale.IUTomm( coords[ii].y - aOrigin.y ), aZposition );
1843 }
1844
1845 // Build OpenCascade shape outlines
1846 BRepBuilderAPI_MakeWire wire;
1847 bool success = true;
1848
1849 // Short segments (distance between end points < m_mergeOCCMaxDist(in mm)) must be
1850 // skipped because OCC merge end points, and a null shape is created
1851 bool short_seg = pcbIUScale.IUTomm( len ) <= m_mergeOCCMaxDist;
1852
1853 try
1854 {
1855 TopoDS_Edge edge;
1856
1857 if( short_seg )
1858 {
1859 Handle( Geom_Circle ) circle = GC_MakeCircle( coords3D[1], // arc1 start point
1860 coords3D[2], // arc1 mid point
1861 coords3D[5] // arc2 mid point
1862 );
1863
1864 edge = BRepBuilderAPI_MakeEdge( circle );
1865 wire.Add( edge );
1866 }
1867 else
1868 {
1869 edge = BRepBuilderAPI_MakeEdge( coords3D[0], coords3D[1] );
1870 wire.Add( edge );
1871
1872 Handle( Geom_TrimmedCurve ) arcOfCircle =
1873 GC_MakeArcOfCircle( coords3D[1], // start point
1874 coords3D[2], // mid point
1875 coords3D[3] // end point
1876 );
1877 edge = BRepBuilderAPI_MakeEdge( arcOfCircle );
1878 wire.Add( edge );
1879
1880 edge = BRepBuilderAPI_MakeEdge( coords3D[3], coords3D[4] );
1881 wire.Add( edge );
1882
1883 Handle( Geom_TrimmedCurve ) arcOfCircle2 =
1884 GC_MakeArcOfCircle( coords3D[4], // start point
1885 coords3D[5], // mid point
1886 coords3D[0] // end point
1887 );
1888 edge = BRepBuilderAPI_MakeEdge( arcOfCircle2 );
1889 wire.Add( edge );
1890 }
1891 }
1892 catch( const Standard_Failure& e )
1893 {
1894 m_reporter->Report( wxString::Format( _( "OCC exception building shape segment: %s" ),
1895 e.GetMessageString() ),
1897 return false;
1898 }
1899
1900 BRepBuilderAPI_MakeFace face;
1901
1902 try
1903 {
1904 gp_Pln plane( coords3D[0], gp::DZ() );
1905 face = BRepBuilderAPI_MakeFace( plane, wire );
1906 }
1907 catch( const Standard_Failure& e )
1908 {
1909 m_reporter->Report( wxString::Format( _( "OCC exception building face: %s" ),
1910 e.GetMessageString() ),
1912 return false;
1913 }
1914
1915 if( aThickness != 0.0 )
1916 {
1917 aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
1918
1919 if( aShape.IsNull() )
1920 {
1921 m_reporter->Report( _( "Failed to create a prismatic shape" ),
1923 return false;
1924 }
1925 }
1926 else
1927 {
1928 aShape = face;
1929 }
1930
1931 return success;
1932}
1933
1934
1935bool STEP_PCB_MODEL::MakePolygonAsWall( TopoDS_Shape& aShape,
1936 SHAPE_POLY_SET& aPolySet,
1937 double aHeight,
1938 double aZposition, const VECTOR2D& aOrigin )
1939{
1940 std::vector<TopoDS_Shape> testShapes;
1941
1942 bool success = MakeShapes( testShapes, aPolySet, m_simplifyShapes,
1943 aHeight, aZposition, aOrigin );
1944
1945 if( testShapes.size() > 0 )
1946 aShape = testShapes.front();
1947 else
1948 success = false;
1949
1950 return success;
1951}
1952
1953
1954static wxString formatBBox( const BOX2I& aBBox )
1955{
1956 wxString str;
1957 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
1958
1959 str << "x0: " << unitsProvider.StringFromValue( aBBox.GetLeft(), false ) << "; ";
1960 str << "y0: " << unitsProvider.StringFromValue( aBBox.GetTop(), false ) << "; ";
1961 str << "x1: " << unitsProvider.StringFromValue( aBBox.GetRight(), false ) << "; ";
1962 str << "y1: " << unitsProvider.StringFromValue( aBBox.GetBottom(), false );
1963
1964 return str;
1965}
1966
1967
1968static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN& aChain,
1969 double aMergeOCCMaxDist, double aZposition, const VECTOR2D& aOrigin,
1970 REPORTER* aReporter )
1971{
1972 auto toPoint =
1973 [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
1974 {
1975 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
1976 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
1977 };
1978
1979 try
1980 {
1981 auto addSegment = [&]( const VECTOR2I& aPt0, const VECTOR2I& aPt1 ) -> bool
1982 {
1983 if( aPt0 == aPt1 )
1984 return false;
1985
1986 gp_Pnt start = toPoint( aPt0 );
1987 gp_Pnt end = toPoint( aPt1 );
1988
1989 BRepBuilderAPI_MakeEdge mkEdge( start, end );
1990
1991 if( !mkEdge.IsDone() || mkEdge.Edge().IsNull() )
1992 {
1993 aReporter->Report( wxString::Format( _( "Failed to make segment edge (%d %d) -> (%d %d), "
1994 "skipping" ),
1995 aPt0.x, aPt0.y,
1996 aPt1.x, aPt1.y ),
1998 }
1999 else
2000 {
2001 aMkWire.Add( mkEdge.Edge() );
2002
2003 if( aMkWire.Error() != BRepLib_WireDone )
2004 {
2005 aReporter->Report( wxString::Format( _( "Failed to add segment edge (%d %d) -> (%d %d)" ),
2006 aPt0.x, aPt0.y,
2007 aPt1.x, aPt1.y ),
2009 return false;
2010 }
2011 }
2012
2013 return true;
2014 };
2015
2016 auto addArc = [&]( const VECTOR2I& aPt0, const SHAPE_ARC& aArc ) -> bool
2017 {
2018 // Do not export too short segments: they create broken shape because OCC thinks
2019 Handle( Geom_Curve ) curve;
2020
2021 if( aArc.GetCentralAngle() == ANGLE_360 )
2022 {
2023 gp_Ax2 axis = gp::XOY();
2024 axis.SetLocation( toPoint( aArc.GetCenter() ) );
2025
2026 curve = GC_MakeCircle( axis, pcbIUScale.IUTomm( aArc.GetRadius() ) ).Value();
2027 }
2028 else
2029 {
2030 curve = GC_MakeArcOfCircle( toPoint( aPt0 ), toPoint( aArc.GetArcMid() ),
2031 toPoint( aArc.GetP1() ) ).Value();
2032 }
2033
2034 if( curve.IsNull() )
2035 return false;
2036
2037 aMkWire.Add( BRepBuilderAPI_MakeEdge( curve ) );
2038
2039 if( !aMkWire.IsDone() )
2040 {
2041 aReporter->Report( wxString::Format( _( "Failed to add arc curve from (%d %d), arc p0 "
2042 "(%d %d), mid (%d %d), p1 (%d %d)" ),
2043 aPt0.x, aPt0.y,
2044 aArc.GetP0().x, aArc.GetP0().y,
2045 aArc.GetArcMid().x, aArc.GetArcMid().y,
2046 aArc.GetP1().x, aArc.GetP1().y ),
2048 return false;
2049 }
2050
2051 return true;
2052 };
2053
2054 VECTOR2I firstPt;
2055 VECTOR2I lastPt;
2056 bool isFirstShape = true;
2057
2058 for( int i = 0; i <= aChain.PointCount() && i != -1; i = aChain.NextShape( i ) )
2059 {
2060 if( i == 0 )
2061 {
2062 if( aChain.IsArcSegment( 0 ) && aChain.IsArcSegment( aChain.PointCount() - 1 )
2063 && aChain.ArcIndex( 0 ) == aChain.ArcIndex( aChain.PointCount() - 1 ) )
2064 {
2065 // Skip first arc (we should encounter it later)
2066 int nextShape = aChain.NextShape( i );
2067
2068 // If nextShape points to the end, then we have a circle.
2069 if( nextShape != -1 )
2070 i = nextShape;
2071 }
2072 }
2073
2074 if( isFirstShape )
2075 lastPt = aChain.CPoint( i );
2076
2077 bool isArc = aChain.IsArcSegment( i );
2078
2079 if( aChain.IsArcStart( i ) )
2080 {
2081 const SHAPE_ARC& currentArc = aChain.Arc( aChain.ArcIndex( i ) );
2082
2083 if( isFirstShape )
2084 {
2085 firstPt = currentArc.GetP0();
2086 lastPt = firstPt;
2087 }
2088
2089 if( addSegment( lastPt, currentArc.GetP0() ) )
2090 lastPt = currentArc.GetP0();
2091
2092 if( addArc( lastPt, currentArc ) )
2093 lastPt = currentArc.GetP1();
2094 }
2095 else if( !isArc )
2096 {
2097 const SEG& seg = aChain.CSegment( i );
2098
2099 if( isFirstShape )
2100 {
2101 firstPt = seg.A;
2102 lastPt = firstPt;
2103 }
2104
2105 if( addSegment( lastPt, seg.A ) )
2106 lastPt = seg.A;
2107
2108 if( addSegment( lastPt, seg.B ) )
2109 lastPt = seg.B;
2110 }
2111
2112 isFirstShape = false;
2113 }
2114
2115 if( lastPt != firstPt && !addSegment( lastPt, firstPt ) )
2116 {
2117 aReporter->Report( wxString::Format( _( "Failed to close wire at %d, %d -> %d, %d **" ),
2118 lastPt.x, lastPt.y,
2119 firstPt.x, firstPt.y ),
2121
2122 return false;
2123 }
2124 }
2125 catch( const Standard_Failure& e )
2126 {
2127 aReporter->Report( wxString::Format( _( "OCC exception creating wire: %s" ),
2128 e.GetMessageString() ),
2130 return false;
2131 }
2132
2133 return true;
2134}
2135
2136
2137bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet,
2138 bool aConvertToArcs, double aThickness, double aZposition,
2139 const VECTOR2D& aOrigin )
2140{
2141 SHAPE_POLY_SET workingPoly = aPolySet;
2142 workingPoly.Simplify();
2143
2144 SHAPE_POLY_SET fallbackPoly = workingPoly;
2145
2146 if( aConvertToArcs )
2147 {
2148 SHAPE_POLY_SET approximated = workingPoly;
2149
2150 for( size_t polyId = 0; polyId < approximated.CPolygons().size(); polyId++ )
2151 {
2152 SHAPE_POLY_SET::POLYGON& polygon = approximated.Polygon( polyId );
2153
2154 for( size_t contId = 0; contId < polygon.size(); contId++ )
2155 polygon[contId] = approximateLineChainWithArcs( polygon[contId] );
2156 }
2157
2158 fallbackPoly = workingPoly;
2159 workingPoly = approximated;
2160
2161 // TODO: this is not accurate because it doesn't check arcs.
2162 /*if( approximated.IsSelfIntersecting() )
2163 {
2164 m_reporter->Report( wxString::Format( _( "Approximated polygon self-intersection check failed\n"
2165 "z: %g; bounding box: %s" ) ),
2166 aZposition,
2167 formatBBox( workingPoly.BBox() ) ),
2168 RPT_SEVERITY_ERROR );
2169 }
2170 else
2171 {
2172 fallbackPoly = workingPoly;
2173 workingPoly = approximated;
2174 }*/
2175 }
2176
2177#if 0 // No longer in use
2178 auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
2179 {
2180 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
2181 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
2182 };
2183#endif
2184
2185 gp_Pln basePlane( gp_Pnt( 0.0, 0.0, aZposition ),
2186 std::signbit( aThickness ) ? -gp::DZ() : gp::DZ() );
2187
2188 for( size_t polyId = 0; polyId < workingPoly.CPolygons().size(); polyId++ )
2189 {
2190 SHAPE_POLY_SET::POLYGON& polygon = workingPoly.Polygon( polyId );
2191
2192 auto tryMakeWire = [this, &aZposition,
2193 &aOrigin]( const SHAPE_LINE_CHAIN& aContour, bool aAllowRetry ) -> TopoDS_Wire
2194 {
2195 TopoDS_Wire wire;
2196 BRepLib_MakeWire mkWire;
2197
2198 makeWireFromChain( mkWire, aContour, m_mergeOCCMaxDist, aZposition, aOrigin, m_reporter );
2199
2200 if( mkWire.IsDone() )
2201 {
2202 wire = mkWire.Wire();
2203 }
2204 else
2205 {
2206 m_reporter->Report(
2207 wxString::Format( _( "Wire not done (contour points %d): OCC error %d\n"
2208 "z: %g; bounding box: %s" ),
2209 static_cast<int>( aContour.PointCount() ),
2210 static_cast<int>( mkWire.Error() ),
2211 formatBBox( aContour.BBox() ) ),
2213 }
2214
2215 if( !wire.IsNull() )
2216 {
2217 BRepAlgoAPI_Check check( wire, false, true );
2218
2219 if( !check.IsValid() )
2220 {
2221 m_reporter->Report( wxString::Format( _( "Wire self-interference check failed\n"
2222 "z: %g; bounding box: %s" ),
2223 aZposition,
2224 formatBBox( aContour.BBox() ) ),
2226
2227 wire.Nullify();
2228 }
2229 }
2230
2231 return wire;
2232 };
2233
2234 BRepBuilderAPI_MakeFace mkFace;
2235
2236 for( size_t contId = 0; contId < polygon.size(); contId++ )
2237 {
2238 try
2239 {
2240 // We allow retry when trying to convert polygon[contId] when a convert error
2241 // happens, using an equivalent polygon shape.
2242 bool allow_retry = aConvertToArcs ? true : false;
2243
2244 TopoDS_Wire wire = tryMakeWire( polygon[contId], allow_retry );
2245
2246 if( aConvertToArcs && wire.IsNull() )
2247 {
2248 m_reporter->Report( wxString::Format( _( "Using non-simplified polygon." ) ),
2250
2251 // Fall back to original shape. Do not allow retry
2252 allow_retry = false;
2253 wire = tryMakeWire( fallbackPoly.CPolygon( polyId )[contId], allow_retry );
2254 }
2255
2256 if( contId == 0 ) // Outline
2257 {
2258 if( !wire.IsNull() )
2259 {
2260 if( basePlane.Axis().Direction().Z() < 0 )
2261 wire.Reverse();
2262
2263 mkFace = BRepBuilderAPI_MakeFace( basePlane, wire );
2264 }
2265 else
2266 {
2267 m_reporter->Report( wxString::Format( wxT( "** Outline skipped **\n"
2268 "z: %g; bounding box: %s" ),
2269 aZposition,
2270 formatBBox( polygon[contId].BBox() ) ),
2272 break;
2273 }
2274 }
2275 else // Hole
2276 {
2277 if( !wire.IsNull() )
2278 {
2279 if( basePlane.Axis().Direction().Z() > 0 )
2280 wire.Reverse();
2281
2282 mkFace.Add( wire );
2283 }
2284 else
2285 {
2286 m_reporter->Report( wxString::Format( wxT( "** Hole skipped **\n"
2287 "z: %g; bounding box: %s" ),
2288 aZposition,
2289 formatBBox( polygon[contId].BBox() ) ),
2291 }
2292 }
2293 }
2294 catch( const Standard_Failure& e )
2295 {
2296 m_reporter->Report( wxString::Format( _( "OCC exception creating contour %d: %s" ),
2297 static_cast<int>( contId ),
2298 e.GetMessageString() ),
2300 return false;
2301 }
2302 }
2303
2304 if( mkFace.IsDone() )
2305 {
2306 TopoDS_Shape faceShape = mkFace.Shape();
2307
2308 if( aThickness != 0.0 )
2309 {
2310 TopoDS_Shape prism = BRepPrimAPI_MakePrism( faceShape, gp_Vec( 0, 0, aThickness ) );
2311 aShapes.push_back( prism );
2312
2313 if( prism.IsNull() )
2314 {
2315 m_reporter->Report( _( "Failed to create a prismatic shape" ), RPT_SEVERITY_ERROR );
2316 return false;
2317 }
2318 }
2319 else
2320 {
2321 aShapes.push_back( faceShape );
2322 }
2323 }
2324 else
2325 {
2326 m_reporter->Report( _( "** Face skipped **" ), RPT_SEVERITY_DEBUG );
2327 }
2328 }
2329
2330 return true;
2331}
2332
2333
2334// These colors are based on 3D viewer's colors and are different to "gbrjobColors"
2335static std::vector<FAB_LAYER_COLOR> s_soldermaskColors = {
2336 { NotSpecifiedPrm(), wxColor( 20, 51, 36 ) }, // Not specified, not in .gbrjob file
2337 { _HKI( "Green" ), wxColor( 20, 51, 36 ) }, // used in .gbrjob file
2338 { _HKI( "Red" ), wxColor( 181, 19, 21 ) }, // used in .gbrjob file
2339 { _HKI( "Blue" ), wxColor( 2, 59, 162 ) }, // used in .gbrjob file
2340 { _HKI( "Purple" ), wxColor( 32, 2, 53 ) }, // used in .gbrjob file
2341 { _HKI( "Black" ), wxColor( 11, 11, 11 ) }, // used in .gbrjob file
2342 { _HKI( "White" ), wxColor( 245, 245, 245 ) }, // used in .gbrjob file
2343 { _HKI( "Yellow" ), wxColor( 194, 195, 0 ) }, // used in .gbrjob file
2344 { _HKI( "User defined" ), wxColor( 128, 128, 128 ) } // Free; the name is a dummy name here
2345};
2346
2347
2348static bool colorFromStackup( BOARD_STACKUP_ITEM_TYPE aType, const wxString& aColorStr,
2349 COLOR4D& aColorOut )
2350{
2351 if( !IsPrmSpecified( aColorStr ) )
2352 return false;
2353
2354 if( aColorStr.StartsWith( wxT( "#" ) ) ) // User defined color
2355 {
2356 aColorOut = COLOR4D( aColorStr );
2357 return true;
2358 }
2359 else
2360 {
2361 const std::vector<FAB_LAYER_COLOR>& colors =
2362 ( aType == BS_ITEM_TYPE_SOLDERMASK || aType == BS_ITEM_TYPE_SILKSCREEN )
2364 : GetStandardColors( aType );
2365
2366 for( const FAB_LAYER_COLOR& fabColor : colors )
2367 {
2368 if( fabColor.GetName() == aColorStr )
2369 {
2370 aColorOut = fabColor.GetColor( aType );
2371 return true;
2372 }
2373 }
2374 }
2375
2376 return false;
2377}
2378
2379
2380bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, const VECTOR2D& aOrigin, bool aPushBoardBody )
2381{
2382 if( m_hasPCB )
2383 {
2384 if( !isBoardOutlineValid() )
2385 return false;
2386
2387 return true;
2388 }
2389
2391
2392 Handle( XCAFDoc_VisMaterialTool ) visMatTool = XCAFDoc_DocumentTool::VisMaterialTool( m_doc->Main() );
2393
2394 m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
2395
2396 // Support for more than one main outline (more than one board)
2397 m_reporter->Report( wxString::Format( wxT( "Build board outlines (%d outlines) with %d points." ),
2398 aOutline.OutlineCount(),
2399 aOutline.FullPointCount() ),
2401
2402 double boardThickness;
2403 double boardZPos;
2404 getBoardBodyZPlacement( boardZPos, boardThickness );
2405
2406#if 1
2407 // This code should work, and it is working most of time
2408 // However there are issues if the main outline is a circle with holes:
2409 // holes from vias and pads are not working
2410 // see bug https://gitlab.com/kicad/code/kicad/-/issues/17446
2411 // (Holes are missing from STEP export with circular PCB outline)
2412 // Hard to say if the bug is in our code or in OCC 7.7
2413 if( !MakeShapes( m_board_outlines, aOutline, false, boardThickness, boardZPos, aOrigin ) )
2414 {
2415 // Error
2416 m_reporter->Report( _( "OCC error creating main outline." ), RPT_SEVERITY_ERROR );
2417 }
2418#else
2419 // Workaround for bug #17446 Holes are missing from STEP export with circular PCB outline
2420 for( const SHAPE_POLY_SET::POLYGON& polygon : aOutline.CPolygons() )
2421 {
2422 for( size_t contId = 0; contId < polygon.size(); contId++ )
2423 {
2424 const SHAPE_LINE_CHAIN& contour = polygon[contId];
2425 SHAPE_POLY_SET polyset;
2426 polyset.Append( contour );
2427
2428 if( contId == 0 ) // main Outline
2429 {
2430 if( !MakeShapes( m_board_outlines, polyset, false, boardThickness, boardZPos,
2431 aOrigin ) )
2432 {
2433 m_reporter->Report( _( "OCC error creating main outline." ),
2435 }
2436 }
2437 else // Hole inside the main outline
2438 {
2439 if( !MakeShapes( m_boardCutouts, polyset, false, boardThickness, boardZPos,
2440 aOrigin ) )
2441 {
2442 m_reporter->Report( _( "OCC error creating hole in main outline." ),
2444 }
2445 }
2446 }
2447 }
2448#endif
2449
2450 // Even if we've disabled board body export, we still need the shapes for bounding box calculations.
2451 Bnd_Box brdBndBox;
2452
2453 for( const TopoDS_Shape& brdShape : m_board_outlines )
2454 BRepBndLib::Add( brdShape, brdBndBox );
2455
2456 // subtract cutouts (if any)
2457 m_reporter->Report( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s))." ),
2458 (int) ( m_boardCutouts.size() + m_copperCutouts.size() ) ),
2460
2461 auto buildBSB =
2462 [&brdBndBox]( std::vector<TopoDS_Shape>& input, Bnd_BoundSortBox& bsbHoles )
2463 {
2464 // We need to encompass every location we'll need to test in the global bbox,
2465 // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
2466 Bnd_Box brdWithHolesBndBox = brdBndBox;
2467
2468 Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
2469
2470 for( size_t i = 0; i < input.size(); i++ )
2471 {
2472 Bnd_Box bbox;
2473 BRepBndLib::Add( input[i], bbox );
2474 brdWithHolesBndBox.Add( bbox );
2475 ( *holeBoxSet )[i] = bbox;
2476 }
2477
2478 bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
2479 };
2480
2481 auto subtractShapesMap =
2482 [&tp, this]( const wxString& aWhat, std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2483 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles )
2484 {
2485 m_reporter->Report( wxString::Format( _( "Subtracting holes for %s" ), aWhat ),
2487
2488 for( auto& [netname, vec] : aShapesMap )
2489 {
2490 std::mutex mutex;
2491
2492 auto subtractLoopFn = [&]( const int shapeId )
2493 {
2494 TopoDS_Shape& shape = vec[shapeId];
2495
2496 Bnd_Box shapeBbox;
2497 BRepBndLib::Add( shape, shapeBbox );
2498
2499 TopTools_ListOfShape holelist;
2500
2501 {
2502 std::unique_lock lock( mutex );
2503
2504 const TColStd_ListOfInteger& indices = aBSBHoles.Compare( shapeBbox );
2505
2506 for( const Standard_Integer& index : indices )
2507 holelist.Append( aHolesList[index] );
2508 }
2509
2510 if( holelist.IsEmpty() )
2511 return; // nothing to cut for this shape
2512
2513 TopTools_ListOfShape cutArgs;
2514 cutArgs.Append( shape );
2515
2516 BRepAlgoAPI_Cut cut;
2517
2518 cut.SetRunParallel( true );
2519 cut.SetToFillHistory( false );
2520
2521 cut.SetArguments( cutArgs );
2522 cut.SetTools( holelist );
2523 cut.Build();
2524
2525 if( cut.HasErrors() || cut.HasWarnings() )
2526 {
2527 m_reporter->Report( wxString::Format( _( "** Got problems while cutting "
2528 "%s net '%s' **" ),
2529 aWhat,
2530 UnescapeString( netname ) ),
2532 shapeBbox.Dump();
2533
2534 if( cut.HasErrors() )
2535 {
2536 wxString msg = _( "Errors:\n" );
2537 wxStringOutputStream os_stream( &msg );
2538 wxStdOutputStream out( os_stream );
2539
2540 cut.DumpErrors( out );
2541 m_reporter->Report( msg, RPT_SEVERITY_WARNING);
2542 }
2543
2544 if( cut.HasWarnings() )
2545 {
2546 wxString msg = _( "Warnings:\n" );
2547 wxStringOutputStream os_stream( &msg );
2548 wxStdOutputStream out( os_stream );
2549
2550 cut.DumpWarnings( out );
2551 m_reporter->Report( msg, RPT_SEVERITY_WARNING );
2552 }
2553 }
2554
2555 shape = cut.Shape();
2556 };
2557
2558 tp.submit_loop( 0, vec.size(), subtractLoopFn ).wait();
2559 }
2560 };
2561
2562 auto subtractShapes =
2563 [subtractShapesMap]( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
2564 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles )
2565 {
2566 std::map<wxString, std::vector<TopoDS_Shape>> aShapesMap{ { wxEmptyString, aShapesList } };
2567
2568 subtractShapesMap( aWhat, aShapesMap, aHolesList, aBSBHoles );
2569 aShapesList = aShapesMap[wxEmptyString];
2570 };
2571
2572
2573 if( m_boardCutouts.size() )
2574 {
2575 Bnd_BoundSortBox bsbHoles;
2576 buildBSB( m_boardCutouts, bsbHoles );
2577
2578 subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles );
2579 }
2580
2581 if( m_copperCutouts.size() )
2582 {
2583 Bnd_BoundSortBox bsbHoles;
2584 buildBSB( m_copperCutouts, bsbHoles );
2585
2586 subtractShapesMap( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles );
2587 subtractShapesMap( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles );
2588 }
2589
2590 if( m_fuseShapes )
2591 {
2592 std::map<wxString, TopTools_ListOfShape> shapesToFuseMap;
2593
2594 auto addShapes = [&shapesToFuseMap]( const wxString& aNetname,
2595 const std::vector<TopoDS_Shape>& aShapes )
2596 {
2597 for( const TopoDS_Shape& shape : aShapes )
2598 shapesToFuseMap[aNetname].Append( shape );
2599 };
2600
2601 for( const auto& [netname, shapes] : m_board_copper )
2602 addShapes( netname, shapes );
2603
2604 for( const auto& [netname, shapes] : m_board_copper_pads )
2605 addShapes( netname, shapes );
2606
2607 for( const auto& [netname, shapes] : m_board_copper_vias )
2608 addShapes( netname, shapes );
2609
2610 m_reporter->Report( wxT( "Fusing shapes" ), RPT_SEVERITY_DEBUG );
2611
2612 // Do fusing in parallel
2613 std::mutex mutex;
2614
2615 auto fuseLoopFn = [&]( const wxString& aNetname )
2616 {
2617 auto& toFuse = shapesToFuseMap[aNetname];
2618 TopoDS_Shape fusedShape = fuseShapesOrCompound( toFuse, m_reporter );
2619
2620 if( !fusedShape.IsNull() )
2621 {
2622 std::unique_lock lock( mutex );
2623
2624 m_board_copper_fused[aNetname].emplace_back( fusedShape );
2625
2626 m_board_copper[aNetname].clear();
2627 m_board_copper_pads[aNetname].clear();
2628 m_board_copper_vias[aNetname].clear();
2629 }
2630 };
2631
2632 BS::multi_future<void> mf;
2633
2634 for( const auto& [netname, _] : shapesToFuseMap )
2635 mf.push_back( tp.submit_task( [&, netname]() { fuseLoopFn( netname ); } ) );
2636
2637 mf.wait();
2638 }
2639
2640 // push the board to the data structure
2641 m_reporter->Report( wxT( "Generate board full shape." ), RPT_SEVERITY_DEBUG );
2642
2643 // AddComponent adds a label that has a reference (not a parent/child relation) to the real
2644 // label. We need to extract that real label to name it for the STEP output cleanly
2645 // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
2646 // like "deduplicate" imported STEPs by swapping STEP assembly components with already
2647 // identically named assemblies. So we want to avoid having the PCB be generally defaulted
2648 // to "Component" or "Assembly".
2649
2650 // aCompoundNets will place all geometry within a net into one compound.
2651 // aCompoundAll will place all geometry into one compound.
2652 auto pushToAssemblyMap =
2653 [&]( const std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2654 const TDF_Label& aVisMatLabel, const wxString& aShapeName, bool aCompoundNets,
2655 bool aCompoundAll, const wxString& aNiceName )
2656 {
2657 std::map<wxString, std::vector<TopoDS_Shape>> shapesMap;
2658
2659 if( aCompoundAll )
2660 {
2661 std::vector<TopoDS_Shape> allShapes;
2662
2663 for( const auto& [netname, shapesList] : aShapesMap )
2664 allShapes.insert( allShapes.end(), shapesList.begin(), shapesList.end() );
2665
2666 if( !allShapes.empty() )
2667 shapesMap[wxEmptyString].emplace_back( makeCompound( allShapes ) );
2668 }
2669 else
2670 {
2671 shapesMap = aShapesMap;
2672 }
2673
2674 for( const auto& [netname, shapesList] : shapesMap )
2675 {
2676 std::vector<TopoDS_Shape> newList;
2677
2678 if( aCompoundNets )
2679 newList.emplace_back( makeCompound( shapesList ) );
2680 else
2681 newList = shapesList;
2682
2683 int i = 1;
2684
2685 for( TopoDS_Shape& shape : newList )
2686 {
2687 Handle( TDataStd_TreeNode ) node;
2688
2689 // Dont expand the component or else coloring it gets hard
2690 TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
2691 KICAD3D_INFO::Set( lbl, KICAD3D_MODEL_TYPE::BOARD, aNiceName.ToStdString() );
2692 m_pcb_labels.push_back( lbl );
2693
2694 if( m_pcb_labels.back().IsNull() )
2695 return;
2696
2697 lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
2698 TDF_Label shpLbl = node->Father()->Label();
2699
2700 if( !shpLbl.IsNull() )
2701 {
2702 if( visMatTool && !aVisMatLabel.IsNull() )
2703 visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
2704
2705 wxString shapeName;
2706
2707 shapeName << m_pcbName;
2708 shapeName << '_';
2709 shapeName << aShapeName;
2710
2711 if( !netname.empty() )
2712 {
2713 shapeName << '_';
2714 shapeName << netname;
2715 }
2716
2717 if( newList.size() > 1 )
2718 {
2719 shapeName << '_';
2720 shapeName << i;
2721 }
2722
2723 TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
2724 TDataStd_Name::Set( shpLbl, partname );
2725 }
2726
2727 i++;
2728 }
2729 }
2730 };
2731
2732 auto pushToAssembly =
2733 [&]( const std::vector<TopoDS_Shape>& aShapesList, const TDF_Label& aVisMatLabel,
2734 const wxString& aShapeName, bool aCompound, const wxString& aNiceName )
2735 {
2736 const std::map<wxString, std::vector<TopoDS_Shape>> shapesMap{ { wxEmptyString, aShapesList } };
2737
2738 pushToAssemblyMap( shapesMap, aVisMatLabel, aShapeName, aCompound, aCompound, aNiceName );
2739 };
2740
2741 auto makeMaterial =
2742 [&]( const TCollection_AsciiString& aName, const Quantity_ColorRGBA& aBaseColor,
2743 double aMetallic, double aRoughness ) -> TDF_Label
2744 {
2745 Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
2746 XCAFDoc_VisMaterialPBR pbr;
2747 pbr.BaseColor = aBaseColor;
2748 pbr.Metallic = aMetallic;
2749 pbr.Roughness = aRoughness;
2750 vismat->SetPbrMaterial( pbr );
2751 return visMatTool->AddMaterial( vismat, aName );
2752 };
2753
2754 // Init colors for the board items
2755 Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
2756 Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
2757
2758 Quantity_ColorRGBA board_color( 0.42f, 0.45f, 0.29f, 0.98f );
2759 Quantity_ColorRGBA front_silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
2760 Quantity_ColorRGBA back_silk_color = front_silk_color;
2761 Quantity_ColorRGBA front_mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
2762 Quantity_ColorRGBA back_mask_color = front_mask_color;
2763
2764 // Get colors from stackup
2765 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
2766 {
2767 COLOR4D col;
2768
2769 if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
2770 continue;
2771
2772 if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
2773 {
2774 col.Darken( 0.2 );
2775
2776 if( item->GetBrdLayerId() == F_Mask )
2777 front_mask_color.SetValues( col.r, col.g, col.b, col.a );
2778 else
2779 back_mask_color.SetValues( col.r, col.g, col.b, col.a );
2780 }
2781
2782 if( item->GetBrdLayerId() == F_SilkS )
2783 front_silk_color.SetValues( col.r, col.g, col.b, col.a );
2784 else if( item->GetBrdLayerId() == B_SilkS )
2785 back_silk_color.SetValues( col.r, col.g, col.b, col.a );
2786
2787 if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
2788 board_color.SetValues( col.r, col.g, col.b, col.a );
2789 }
2790
2791 // Paint board body in soldermask colors if soldermask is not exported as a layer
2792 if( !m_enabledLayers.Contains( F_Mask ) && !m_enabledLayers.Contains( B_Mask ) )
2793 {
2794 board_color = front_mask_color;
2795 board_color.SetAlpha( 1.0 );
2796 }
2797
2798 TDF_Label front_mask_mat = makeMaterial( "soldermask", front_mask_color, 0.0, 0.6 );
2799 TDF_Label back_mask_mat = makeMaterial( "soldermask", back_mask_color, 0.0, 0.6 );
2800 TDF_Label front_silk_mat = makeMaterial( "silkscreen", front_silk_color, 0.0, 0.9 );
2801 TDF_Label back_silk_mat = makeMaterial( "silkscreen", back_silk_color, 0.0, 0.9 );
2802 TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
2803 TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
2804 TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
2805
2806 pushToAssemblyMap( m_board_copper, copper_mat, "copper", true, true, "Copper" );
2807 pushToAssemblyMap( m_board_copper_pads, pad_mat, "pad", true, true, "Pads" );
2808 pushToAssemblyMap( m_board_copper_vias, copper_mat, "via", true, true, "Via" );
2809 pushToAssemblyMap( m_board_copper_fused, copper_mat, "copper", true, true, "Copper" );
2810 pushToAssembly( m_board_front_silk, front_silk_mat, "silkscreen", true, "Top Silkscreen" );
2811 pushToAssembly( m_board_back_silk, back_silk_mat, "silkscreen", true, "Bottom Silkscreen" );
2812 pushToAssembly( m_board_front_mask, front_mask_mat, "soldermask", true, "Top Soldermask" );
2813 pushToAssembly( m_board_back_mask, back_mask_mat, "soldermask", true, "Bottom Soldermask" );
2814
2815 if( aPushBoardBody )
2816 pushToAssembly( m_board_outlines, board_mat, "PCB", false, "Body" );
2817
2818#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
2819 m_assy->UpdateAssemblies();
2820#endif
2821
2822 return true;
2823}
2824
2825
2826#ifdef SUPPORTS_IGES
2827// write the assembly model in IGES format
2828bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
2829{
2830 if( !isBoardOutlineValid() )
2831 {
2832 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
2833 aFileName ),
2835 return false;
2836 }
2837
2839
2840 wxFileName fn( aFileName );
2841 IGESControl_Controller::Init();
2842 IGESCAFControl_Writer writer;
2843 writer.SetColorMode( Standard_True );
2844 writer.SetNameMode( Standard_True );
2845 IGESData_GlobalSection header = writer.Model()->GlobalSection();
2846 header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
2847 header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
2848 header.SetAuthorName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
2849 header.SetCompanyName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
2850 writer.Model()->SetGlobalSection( header );
2851
2852 if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
2853 return false;
2854
2855 return true;
2856}
2857#endif
2858
2859bool STEP_PCB_MODEL::CompressSTEP( wxString& inputFile, wxString& outputFile )
2860{
2861 wxFileInputStream input( inputFile );
2862 wxFileOutputStream output( outputFile );
2863
2864 if( !input.IsOk() )
2865 {
2866 m_reporter->Report( wxString::Format( _( "Cannot create input stream '%s'.\n" ), inputFile ) );
2867 return false;
2868 }
2869
2870 if( !output.IsOk() )
2871 {
2872 m_reporter->Report( wxString::Format( _( "Cannot create output stream '%s'.\n" ), outputFile ) );
2873 return false;
2874 }
2875
2876 wxZlibOutputStream zlibStream( output, -1, wxZLIB_GZIP );
2877
2878 if( !zlibStream.IsOk() )
2879 {
2880 m_reporter->Report( _( "Impossible create compress stream" ) );
2881 return false;
2882 }
2883
2884 input.Read( zlibStream );
2885
2886 if( input.LastRead() == 0 || zlibStream.LastWrite() == 0 )
2887 {
2888 m_reporter->Report( _( "Compress read or write error" ) );
2889 return false;
2890 }
2891
2892 zlibStream.Close();
2893 output.Close();
2894
2895 return true;
2896}
2897
2898bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize, bool compress )
2899{
2900 if( !isBoardOutlineValid() )
2901 {
2902 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
2903 aFileName ),
2905 return false;
2906 }
2907
2909
2910 wxFileName fn( aFileName );
2911
2912 STEPCAFControl_Writer writer;
2913 writer.SetColorMode( Standard_True );
2914 writer.SetNameMode( Standard_True );
2915
2916 // This must be set before we "transfer" the document.
2917 // Should default to kicad_pcb.general.title_block.title,
2918 // but in the meantime, defaulting to the basename of the output
2919 // target is still better than "open cascade step translter v..."
2920 // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
2921 if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
2922 {
2923 m_reporter->Report( _( "Failed to set STEP product name, but will attempt to continue." ),
2925 }
2926
2927 // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
2928 // But there are reports that this mode might be less compatible in some cases.
2929 if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
2930 {
2931 m_reporter->Report( _( "Failed to set surface curve mode, but will attempt to continue." ),
2933 }
2934
2935 if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
2936 return false;
2937
2938 APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
2939
2940 // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
2941 // are creating issues in the step file
2942 hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
2943
2944 // TODO: how to control and ensure consistency with IGES?
2945 hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
2946 hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
2947 hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
2948 hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
2949
2950 bool success = true;
2951
2952 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
2953 wxString currCWD = wxGetCwd();
2954 wxString workCWD = fn.GetPath();
2955
2956 if( !workCWD.IsEmpty() )
2957 wxSetWorkingDirectory( workCWD );
2958
2959 wxString tmpfname( "$tempfile$.step" );
2960
2961 if( Standard_False == writer.Write( tmpfname.c_str() ) )
2962 success = false;
2963
2964 if( compress && success )
2965 {
2966 wxString srcTmp( tmpfname );
2967 wxString dstTmp( "$tempfile$.stpz" );
2968
2969 success = STEP_PCB_MODEL::CompressSTEP( srcTmp, dstTmp );
2970 wxRemoveFile( srcTmp );
2971
2972 tmpfname = dstTmp;
2973 }
2974
2975 if( success )
2976 {
2977
2978 // Preserve the permissions of the current file
2979 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname.c_str() );
2980
2981 if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
2982 {
2983 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
2984 tmpfname,
2985 fn.GetFullName() ),
2987 success = false;
2988 }
2989 }
2990
2991 wxSetWorkingDirectory( currCWD );
2992
2993 return success;
2994}
2995
2996
2997bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
2998{
2999 if( !isBoardOutlineValid() )
3000 {
3001 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3002 aFileName ),
3004 return false;
3005 }
3006
3008
3009 // s_assy = shape tool for the source
3010 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3011
3012 // retrieve assembly as a single shape
3013 TopoDS_Shape shape = getOneShape( s_assy );
3014
3015 wxFileName fn( aFileName );
3016
3017 wxFFileOutputStream ffStream( fn.GetFullPath() );
3018 wxStdOutputStream stdStream( ffStream );
3019
3020#if OCC_VERSION_HEX >= 0x070600
3021 BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
3022#else
3023 BRepTools::Write( shape, stdStream );
3024#endif
3025
3026 return true;
3027}
3028
3029
3030bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
3031{
3032 wxFileName fn( aFileName );
3033
3034 wxFFileOutputStream ffStream( fn.GetFullPath() );
3035 wxStdOutputStream file( ffStream );
3036
3037 if( !ffStream.IsOk() )
3038 {
3039 m_reporter->Report( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ),
3041 return false;
3042 }
3043
3045
3046 // s_assy = shape tool for the source
3047 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3048
3049 // retrieve assembly as a single shape
3050 const TopoDS_Shape shape = getOneShape( s_assy );
3051
3052 std::map<wxString, std::vector<int>> groups[4];
3053 std::map<wxString, double> groupAreas;
3054 TopExp_Explorer exp;
3055 int faceIndex = 0;
3056
3057 for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
3058 {
3059 TopoDS_Shape subShape = exp.Current();
3060
3061 Bnd_Box bbox;
3062 BRepBndLib::Add( subShape, bbox );
3063
3064 for( const auto& [padKey, pairs] : m_pad_points )
3065 {
3066 for( const auto& pair : pairs )
3067 {
3068 const auto& [point, padTestShape] = pair;
3069
3070 if( bbox.IsOut( point ) )
3071 continue;
3072
3073 BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
3074
3075 if( surface.GetType() != GeomAbs_Plane )
3076 continue;
3077
3078 BRepExtrema_DistShapeShape dist( padTestShape, subShape );
3079 dist.Perform();
3080
3081 if( !dist.IsDone() )
3082 continue;
3083
3084 if( dist.Value() < Precision::Approximation() )
3085 {
3086 // Push as a face group
3087 groups[2][padKey].push_back( faceIndex );
3088
3089 GProp_GProps system;
3090 BRepGProp::SurfaceProperties( subShape, system );
3091
3092 double surfaceArea = system.Mass() / 1e6; // Convert to meters^2
3093 groupAreas[padKey] += surfaceArea;
3094 }
3095 }
3096 }
3097
3098 faceIndex++;
3099 }
3100
3101 // Based on Gmsh code
3102 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
3103 file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
3104 file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
3105 file << " <shape format=\"BREP\"><![CDATA[";
3106#if OCC_VERSION_HEX < 0x070600
3107 BRepTools::Write( shape, file );
3108#else
3109 BRepTools::Write( shape, file, Standard_True, Standard_True, TopTools_FormatVersion_VERSION_1 );
3110#endif
3111 file << "]]></shape>" << std::endl;
3112 file << " <topology>" << std::endl;
3113
3114 TopTools_IndexedMapOfShape mainMap;
3115 TopExp::MapShapes( shape, mainMap );
3116 std::set<int> topo[4];
3117
3118 static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
3119 TopAbs_SOLID };
3120
3121 static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
3122 static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
3123
3124 for( int dim = 0; dim < 4; dim++ )
3125 {
3126 for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
3127 {
3128 TopoDS_Shape subShape = exp.Current();
3129 int idx = mainMap.FindIndex( subShape );
3130
3131 if( idx && !topo[dim].count( idx ) )
3132 topo[dim].insert( idx );
3133 }
3134 }
3135
3136 for( int dim = 0; dim <= 3; dim++ )
3137 {
3138 std::string labels = c_dimLabels[dim];
3139 std::string label = c_dimLabel[dim];
3140
3141 file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
3142 int index = 0;
3143
3144 for( auto p : topo[dim] )
3145 {
3146 std::string name( "" );
3147 file << " <" << label << " index=\"" << index << "\" "
3148 << "name=\"" << name << "\" "
3149 << "reference=\"" << p << "\"/>" << std::endl;
3150
3151 index++;
3152 }
3153 file << " </" << labels << ">" << std::endl;
3154 }
3155
3156 file << " </topology>" << std::endl;
3157 file << " </geometry>" << std::endl;
3158 file << " <groups count=\""
3159 << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
3160 << std::endl;
3161
3162 int groupNumber = 1;
3163
3164 m_reporter->Report( wxT( "Pad definitions:" ), RPT_SEVERITY_DEBUG );
3165 m_reporter->Report( wxT( "Number\tName\tArea (m^2)" ), RPT_SEVERITY_DEBUG );
3166
3167 for( int dim = 0; dim <= 3; dim++ )
3168 {
3169 std::string label = c_dimLabel[dim];
3170
3171 for( auto g : groups[dim] )
3172 {
3173 //std::string name = model->getPhysicalName( dim, g.first );
3174 wxString name = g.first;
3175
3176 if( name.empty() )
3177 { // create same unique name as for MED export
3178 std::ostringstream gs;
3179 gs << "G_" << dim << "D_" << g.first;
3180 name = gs.str();
3181 }
3182 file << " <group name=\"" << name << "\" dimension=\"" << label;
3183//#if 1
3184// // Gmsh XAO extension: also save the physical tag, so that XAO can be used
3185// // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
3186// file << "\" tag=\"" << g.first;
3187//#endif
3188 file << "\" count=\"" << g.second.size() << "\">" << std::endl;
3189
3190 for( auto index : g.second )
3191 file << " <element index=\"" << index << "\"/>" << std::endl;
3192
3193 file << " </group>" << std::endl;
3194
3195 m_reporter->Report( wxString::Format( "%d\t%s\t%g",
3196 groupNumber,
3197 name,
3198 groupAreas[name] ),
3200
3201 groupNumber++;
3202 }
3203 }
3204
3205 m_reporter->Report( wxT( "" ), RPT_SEVERITY_DEBUG );
3206
3207 file << " </groups>" << std::endl;
3208 file << " <fields count=\"0\"/>" << std::endl;
3209 file << "</XAO>" << std::endl;
3210
3211 return true;
3212}
3213
3214
3215bool STEP_PCB_MODEL::getModelLabel( const std::string& aFileNameUTF8, const VECTOR3D& aScale, TDF_Label& aLabel,
3216 bool aSubstituteModels, wxString* aErrorMessage )
3217{
3218 std::string model_key = aFileNameUTF8 + "_" + std::to_string( aScale.x )
3219 + "_" + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
3220
3221 MODEL_MAP::const_iterator mm = m_models.find( model_key );
3222
3223 if( mm != m_models.end() )
3224 {
3225 aLabel = mm->second;
3226 return true;
3227 }
3228
3229 aLabel.Nullify();
3230
3231 Handle( TDocStd_Document ) doc;
3232 m_app->NewDocument( "MDTV-XCAF", doc );
3233
3234 wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
3235 MODEL3D_FORMAT_TYPE modelFmt = fileType( aFileNameUTF8.c_str() );
3236
3237 wxFileName modelFile( fileName );
3238 std::string pname( modelFile.GetName().ToUTF8() );
3239 TCollection_ExtendedString partname( pname.c_str() );
3240
3241 switch( modelFmt )
3242 {
3243 case FMT_IGES:
3244 if( !readIGES( doc, aFileNameUTF8.c_str() ) )
3245 {
3246 m_reporter->Report( wxString::Format( wxT( "readIGES() failed on filename '%s'." ),
3247 fileName ),
3249 return false;
3250 }
3251 break;
3252
3253 case FMT_STEP:
3254 if( !readSTEP( doc, aFileNameUTF8.c_str() ) )
3255 {
3256 m_reporter->Report( wxString::Format( wxT( "readSTEP() failed on filename '%s'." ),
3257 fileName ),
3259 return false;
3260 }
3261 break;
3262
3263 case FMT_STEPZ:
3264 {
3265 // To export a compressed step file (.stpz or .stp.gz file), the best way is to
3266 // decaompress it in a temporaty file and load this temporary file
3267 wxFFileInputStream ifile( fileName );
3268 wxFileName outFile( fileName );
3269
3270 outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
3271 outFile.SetExt( wxT( "step" ) );
3272 wxFileOffset size = ifile.GetLength();
3273
3274 if( size == wxInvalidOffset )
3275 {
3276 m_reporter->Report( wxString::Format( wxT( "getModelLabel() failed on filename '%s'." ),
3277 fileName ),
3279 return false;
3280 }
3281
3282 {
3283 bool success = false;
3284 wxFFileOutputStream ofile( outFile.GetFullPath() );
3285
3286 if( !ofile.IsOk() )
3287 return false;
3288
3289 char* buffer = new char[size];
3290
3291 ifile.Read( buffer, size );
3292 std::string expanded;
3293
3294 try
3295 {
3296 expanded = gzip::decompress( buffer, size );
3297 success = true;
3298 }
3299 catch( ... )
3300 {
3301 m_reporter->Report( wxString::Format( wxT( "failed to decompress '%s'." ),
3302 fileName ),
3304 }
3305
3306 if( expanded.empty() )
3307 {
3308 ifile.Reset();
3309 ifile.SeekI( 0 );
3310 wxZipInputStream izipfile( ifile );
3311 std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
3312
3313 if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
3314 {
3315 izipfile.Read( ofile );
3316 success = true;
3317 }
3318 }
3319 else
3320 {
3321 ofile.Write( expanded.data(), expanded.size() );
3322 }
3323
3324 delete[] buffer;
3325 ofile.Close();
3326
3327 if( success )
3328 {
3329 std::string altFileNameUTF8 = TO_UTF8( outFile.GetFullPath() );
3330 success = getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
3331 }
3332
3333 return success;
3334 }
3335
3336 break;
3337 }
3338
3339 case FMT_WRL:
3340 case FMT_WRZ:
3341 /* WRL files are preferred for internal rendering, due to superior material properties, etc.
3342 * However they are not suitable for MCAD export.
3343 *
3344 * If a .wrl file is specified, attempt to locate a replacement file for it.
3345 *
3346 * If a valid replacement file is found, the label for THAT file will be associated with
3347 * the .wrl file
3348 */
3349 if( aSubstituteModels )
3350 {
3351 wxFileName wrlName( fileName );
3352
3353 wxString basePath = wrlName.GetPath();
3354 wxString baseName = wrlName.GetName();
3355
3356 // List of alternate files to look for
3357 // Given in order of preference
3358 // (Break if match is found)
3359 wxArrayString alts;
3360
3361 // Step files
3362 alts.Add( wxT( "stp" ) );
3363 alts.Add( wxT( "step" ) );
3364 alts.Add( wxT( "STP" ) );
3365 alts.Add( wxT( "STEP" ) );
3366 alts.Add( wxT( "Stp" ) );
3367 alts.Add( wxT( "Step" ) );
3368 alts.Add( wxT( "stpz" ) );
3369 alts.Add( wxT( "stpZ" ) );
3370 alts.Add( wxT( "STPZ" ) );
3371 alts.Add( wxT( "step.gz" ) );
3372 alts.Add( wxT( "stp.gz" ) );
3373
3374 // IGES files
3375 alts.Add( wxT( "iges" ) );
3376 alts.Add( wxT( "IGES" ) );
3377 alts.Add( wxT( "igs" ) );
3378 alts.Add( wxT( "IGS" ) );
3379
3380 //TODO - Other alternative formats?
3381
3382 for( const auto& alt : alts )
3383 {
3384 wxFileName altFile( basePath, baseName + wxT( "." ) + alt );
3385
3386 if( altFile.IsOk() && altFile.FileExists() )
3387 {
3388 std::string altFileNameUTF8 = TO_UTF8( altFile.GetFullPath() );
3389
3390 // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
3391 // to the new STEP model. This process of auto-substitution is janky as all
3392 // heck so let's not mix up un-displayed scale factors with potentially
3393 // mis-matched files. And hope that the user doesn't have multiples files
3394 // named "model.wrl" and "model.stp" referring to different parts.
3395 // TODO: Fix model handling in v7. Default models should only be STP.
3396 // Have option to override this in DISPLAY.
3397 if( getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
3398 {
3399 return true;
3400 }
3401 }
3402 }
3403 }
3404
3405 // VRML models only work when exporting to mesh formats
3406 // Also OCCT < 7.9.0 fails to load most VRML 2.0 models because of Switch nodes
3410 {
3411 if( readVRML( doc, aFileNameUTF8.c_str() ) )
3412 {
3413 Handle( XCAFDoc_ShapeTool ) shapeTool = XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
3414
3415 prefixNames( shapeTool->Label(), partname );
3416 }
3417 else
3418 {
3419 m_reporter->Report( wxString::Format( wxT( "readVRML() failed on filename '%s'." ), fileName ),
3421 return false;
3422 }
3423 }
3424 else
3425 {
3426 if( aErrorMessage )
3427 aErrorMessage->Printf( _( "Cannot use VRML models when exporting to non-mesh formats." ) );
3428
3429 return false;
3430 }
3431
3432 break;
3433
3434 // TODO: implement IDF and EMN converters
3435
3436 default:
3437 m_reporter->Report( wxString::Format( _( "Cannot identify actual file type for '%s'." ),
3438 fileName ),
3440 return false;
3441 }
3442
3443 aLabel = transferModel( doc, m_doc, aScale );
3444
3445 if( aLabel.IsNull() )
3446 {
3447 m_reporter->Report( wxString::Format( _( "Could not transfer model data from file '%s'." ),
3448 fileName ),
3450 return false;
3451 }
3452
3453 // attach the PART NAME ( base filename: note that in principle
3454 // different models may have the same base filename )
3455 TDataStd_Name::Set( aLabel, partname );
3456
3457 m_models.insert( MODEL_DATUM( model_key, aLabel ) );
3458 ++m_components;
3459 return true;
3460}
3461
3462
3463bool STEP_PCB_MODEL::getModelLocation( bool aBottom, const VECTOR2D& aPosition, double aRotation,
3464 const VECTOR3D& aOffset, const VECTOR3D& aOrientation,
3465 TopLoc_Location& aLocation )
3466{
3467 // Order of operations:
3468 // a. aOrientation is applied -Z*-Y*-X
3469 // b. aOffset is applied
3470 // Top ? add thickness to the Z offset
3471 // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
3472 // then rotate on +Z
3473 // Top ? rotate on -Z
3474 // d. aPosition is applied
3475 //
3476 // Note: Y axis is inverted in KiCad
3477
3478 gp_Trsf lPos;
3479 lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
3480
3481 // Offset board thickness
3482 VECTOR3D offset( aOffset );
3483 offset.z += BOARD_OFFSET;
3484
3485 double boardThickness;
3486 double boardZPos;
3487 getBoardBodyZPlacement( boardZPos, boardThickness );
3488 double top = std::max( boardZPos, boardZPos + boardThickness );
3489 double bottom = std::min( boardZPos, boardZPos + boardThickness );
3490
3491 // 3D step models are placed on the top of copper layers.
3492 // This is true for SMD shapes, and perhaps not always true for TH shapes,
3493 // but we use this Z position for any 3D shape.
3494 double f_pos, f_thickness;
3495 getLayerZPlacement( F_Cu, f_pos, f_thickness );
3496 top += f_thickness;
3497 getLayerZPlacement( B_Cu, f_pos, f_thickness );
3498 bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
3499
3500 gp_Trsf lRot;
3501
3502 if( aBottom )
3503 {
3504 offset.z -= bottom;
3505 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3506 lPos.Multiply( lRot );
3507 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
3508 lPos.Multiply( lRot );
3509 }
3510 else
3511 {
3512 offset.z += top;
3513 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3514 lPos.Multiply( lRot );
3515 }
3516
3517 gp_Trsf lOff;
3518 lOff.SetTranslation( gp_Vec( offset.x, offset.y, offset.z ) );
3519 lPos.Multiply( lOff );
3520
3521 gp_Trsf lOrient;
3522 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), -aOrientation.z );
3523 lPos.Multiply( lOrient );
3524 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ), -aOrientation.y );
3525 lPos.Multiply( lOrient );
3526 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), -aOrientation.x );
3527 lPos.Multiply( lOrient );
3528
3529 aLocation = TopLoc_Location( lPos );
3530 return true;
3531}
3532
3533
3534bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
3535{
3536 IGESControl_Controller::Init();
3537 IGESCAFControl_Reader reader;
3538 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3539
3540 if( stat != IFSelect_RetDone )
3541 return false;
3542
3543 // Enable user-defined shape precision
3544 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3545 return false;
3546
3547 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3548 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3549 return false;
3550
3551 // set other translation options
3552 reader.SetColorMode( true ); // use model colors
3553 reader.SetNameMode( false ); // don't use IGES label names
3554 reader.SetLayerMode( false ); // ignore LAYER data
3555
3556 if( !reader.Transfer( doc ) )
3557 {
3558 if( doc->CanClose() == CDM_CCS_OK )
3559 doc->Close();
3560
3561 return false;
3562 }
3563
3564 // are there any shapes to translate?
3565 if( reader.NbShapes() < 1 )
3566 {
3567 if( doc->CanClose() == CDM_CCS_OK )
3568 doc->Close();
3569
3570 return false;
3571 }
3572
3573 return true;
3574}
3575
3576
3577bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
3578{
3579 STEPCAFControl_Reader reader;
3580 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3581
3582 if( stat != IFSelect_RetDone )
3583 return false;
3584
3585 // Enable user-defined shape precision
3586 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3587 return false;
3588
3589 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3590 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3591 return false;
3592
3593 // set other translation options
3594 reader.SetColorMode( true ); // use model colors
3595 reader.SetNameMode( true ); // use label names
3596 reader.SetLayerMode( false ); // ignore LAYER data
3597
3598 if( !reader.Transfer( doc ) )
3599 {
3600 if( doc->CanClose() == CDM_CCS_OK )
3601 doc->Close();
3602
3603 return false;
3604 }
3605
3606 // are there any shapes to translate?
3607 if( reader.NbRootsForTransfer() < 1 )
3608 {
3609 if( doc->CanClose() == CDM_CCS_OK )
3610 doc->Close();
3611
3612 return false;
3613 }
3614
3615 return true;
3616}
3617
3618
3619bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
3620{
3621#if OCC_VERSION_HEX >= 0x070700
3622 VrmlAPI_CafReader reader;
3623 RWMesh_CoordinateSystemConverter conv;
3624 conv.SetInputLengthUnit( 2.54 );
3625 reader.SetCoordinateSystemConverter( conv );
3626 reader.SetDocument( doc );
3627
3628 if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
3629 return false;
3630
3631 return true;
3632#else
3633 return false;
3634#endif
3635}
3636
3637
3638TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
3639 Handle( TDocStd_Document ) & dest, const VECTOR3D& aScale )
3640{
3641 // transfer data from Source into a top level component of Dest
3642 // s_assy = shape tool for the source
3643 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
3644
3645 // retrieve all free shapes within the assembly
3646 TDF_LabelSequence frshapes;
3647 s_assy->GetFreeShapes( frshapes );
3648
3649 // d_assy = shape tool for the destination
3650 Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
3651
3652 // create a new shape within the destination and set the assembly tool to point to it
3653 TDF_Label d_targetLabel = d_assy->NewShape();
3654
3655 if( frshapes.Size() == 1 )
3656 {
3657 TDocStd_XLinkTool link;
3658 link.Copy( d_targetLabel, frshapes.First() );
3659 }
3660 else
3661 {
3662 // Rare case
3663 for( TDF_Label& s_shapeLabel : frshapes )
3664 {
3665 TDF_Label d_component = d_assy->NewShape();
3666
3667 TDocStd_XLinkTool link;
3668 link.Copy( d_component, s_shapeLabel );
3669
3670 d_assy->AddComponent( d_targetLabel, d_component, TopLoc_Location() );
3671 }
3672 }
3673
3674 if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
3675 rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
3676
3677 return d_targetLabel;
3678}
3679
3680
3681bool STEP_PCB_MODEL::performMeshing( Handle( XCAFDoc_ShapeTool ) & aShapeTool )
3682{
3683 TDF_LabelSequence freeShapes;
3684 aShapeTool->GetFreeShapes( freeShapes );
3685
3686 m_reporter->Report( wxT( "Meshing model" ), RPT_SEVERITY_DEBUG );
3687
3688 // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
3689 // To mesh models, lets just grab the free shape root and execute on them
3690 for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
3691 {
3692 TDF_Label label = freeShapes.Value( i );
3693 TopoDS_Shape shape;
3694 aShapeTool->GetShape( label, shape );
3695
3696 // These deflection values basically affect the accuracy of the mesh generated, a tighter
3697 // deflection will result in larger meshes
3698 // We could make this a tunable parameter, but for now fix it
3699 const Standard_Real linearDeflection = 0.14;
3700 const Standard_Real angularDeflection = DEG2RAD( 30.0 );
3701 BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
3702 Standard_True );
3703 }
3704
3705 return true;
3706}
3707
3708
3709bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
3710{
3711 /*if( !isBoardOutlineValid() )
3712 {
3713 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3714 aFileName ),
3715 RPT_SEVERITY_ERROR );
3716 return false;
3717 }*/
3718
3720
3721 performMeshing( m_assy );
3722
3723 wxFileName fn( aFileName );
3724
3725 const char* tmpGltfname = "$tempfile$.glb";
3726 RWGltf_CafWriter cafWriter( tmpGltfname, true );
3727
3728 cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
3729 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3730 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
3731 RWMesh_CoordinateSystem_Zup );
3732#if OCC_VERSION_HEX >= 0x070700
3733 cafWriter.SetParallel( true );
3734#endif
3735 TColStd_IndexedDataMapOfStringString metadata;
3736
3737 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3738 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3739 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3740 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3741 metadata.Add( TCollection_AsciiString( "generator" ),
3742 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
3743 metadata.Add( TCollection_AsciiString( "generated_at" ),
3744 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3745
3746 bool success = true;
3747
3748 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3749 wxString currCWD = wxGetCwd();
3750 wxString workCWD = fn.GetPath();
3751
3752 if( !workCWD.IsEmpty() )
3753 wxSetWorkingDirectory( workCWD );
3754
3755 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
3756
3757 if( success )
3758 {
3759 // Preserve the permissions of the current file
3760 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
3761
3762 if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
3763 {
3764 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3765 tmpGltfname,
3766 fn.GetFullName() ),
3768 success = false;
3769 }
3770 }
3771
3772 wxSetWorkingDirectory( currCWD );
3773
3774 return success;
3775}
3776
3777
3778bool STEP_PCB_MODEL::WritePLY( const wxString& aFileName )
3779{
3780#if OCC_VERSION_HEX < 0x070700
3781 m_reporter->Report( wxT( "PLY export is not supported before OCCT 7.7.0" ), RPT_SEVERITY_ERROR );
3782 return false;
3783#else
3784
3785 if( !isBoardOutlineValid() )
3786 {
3787 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3788 aFileName ),
3790 return false;
3791 }
3792
3794
3795 performMeshing( m_assy );
3796
3797 wxFileName fn( aFileName );
3798
3799 const char* tmpFname = "$tempfile$.ply";
3800 RWPly_CafWriter cafWriter( tmpFname );
3801
3802 cafWriter.SetFaceId( true ); // TODO: configurable SetPartId/SetFaceId
3803 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3804 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem( RWMesh_CoordinateSystem_Zup );
3805
3806 TColStd_IndexedDataMapOfStringString metadata;
3807
3808 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3809 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3810 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3811 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3812 metadata.Add( TCollection_AsciiString( "generator" ),
3813 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ),
3814 GetSemanticVersion() ).ToAscii() ) );
3815 metadata.Add( TCollection_AsciiString( "generated_at" ),
3816 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3817
3818 bool success = true;
3819
3820 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3821 wxString currCWD = wxGetCwd();
3822 wxString workCWD = fn.GetPath();
3823
3824 if( !workCWD.IsEmpty() )
3825 wxSetWorkingDirectory( workCWD );
3826
3827 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
3828
3829 if( success )
3830 {
3831 // Preserve the permissions of the current file
3832 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
3833
3834 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
3835 {
3836 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3837 tmpFname,
3838 fn.GetFullName() ),
3840 success = false;
3841 }
3842 }
3843
3844 wxSetWorkingDirectory( currCWD );
3845
3846 return success;
3847#endif
3848}
3849
3850
3851bool STEP_PCB_MODEL::WriteSTL( const wxString& aFileName )
3852{
3853 if( !isBoardOutlineValid() )
3854 {
3855 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3856 aFileName ),
3858 return false;
3859 }
3860
3862
3863 performMeshing( m_assy );
3864
3865 wxFileName fn( aFileName );
3866
3867 const char* tmpFname = "$tempfile$.stl";
3868
3869 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3870 wxString currCWD = wxGetCwd();
3871 wxString workCWD = fn.GetPath();
3872
3873 if( !workCWD.IsEmpty() )
3874 wxSetWorkingDirectory( workCWD );
3875
3876 bool success = StlAPI_Writer().Write( getOneShape( m_assy ), tmpFname );
3877
3878 if( success )
3879 {
3880 // Preserve the permissions of the current file
3881 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
3882
3883 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
3884 {
3885 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3886 tmpFname,
3887 fn.GetFullName() ),
3889 success = false;
3890 }
3891 }
3892
3893 wxSetWorkingDirectory( currCWD );
3894
3895 return success;
3896}
3897
3898
3899
3900bool STEP_PCB_MODEL::WriteU3D( const wxString& aFileName )
3901{
3902 if( !isBoardOutlineValid() )
3903 {
3904 m_reporter->Report(
3905 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
3907 return false;
3908 }
3909
3911
3912 performMeshing( m_assy );
3913
3914 wxFileName fn( aFileName );
3915
3916 const char* tmpFname = "$tempfile$.u3d";
3917
3918 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3919 wxString currCWD = wxGetCwd();
3920 wxString workCWD = fn.GetPath();
3921
3922 if( !workCWD.IsEmpty() )
3923 wxSetWorkingDirectory( workCWD );
3924
3925 U3D::WRITER writer( tmpFname );
3926 bool success = writer.Perform( m_doc );
3927 if( success )
3928 {
3929 // Preserve the permissions of the current file
3930 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
3931
3932 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
3933 {
3934 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ), tmpFname,
3935 fn.GetFullName() ),
3937 success = false;
3938 }
3939 }
3940
3941 wxSetWorkingDirectory( currCWD );
3942
3943 return success;
3944}
3945
3946
3947bool STEP_PCB_MODEL::WritePDF( const wxString& aFileName )
3948{
3949 if( !isBoardOutlineValid() )
3950 {
3951 m_reporter->Report(
3952 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
3954 return false;
3955 }
3956
3958
3959 performMeshing( m_assy );
3960
3961 wxFileName fn( aFileName );
3962
3963 wxFileName u3dTmpfn = wxFileName::CreateTempFileName( "" );
3964 wxFileName pdfTmpfn = wxFileName::CreateTempFileName( "" );
3965
3966 U3D::WRITER writer( u3dTmpfn.GetFullPath().ToStdString() );
3967 bool success = writer.Perform( m_doc );
3968
3969 // PDF test
3970 std::unique_ptr<PDF_PLOTTER> plotter = std::make_unique<PDF_PLOTTER>();
3971
3972 plotter->SetColorMode( true );
3973 plotter->Set3DExport( true );
3974 plotter->SetCreator( wxT( "Mark's awesome 3d exporter" ) );
3975 KIGFX::PCB_RENDER_SETTINGS renderSettings;
3976 plotter->SetRenderSettings( &renderSettings );
3977
3978 if( !plotter->OpenFile( pdfTmpfn.GetFullPath() ) )
3979 {
3980 m_reporter->Report( wxString::Format( wxT( "Cannot open temporary file '%s'.\n" ), pdfTmpfn.GetFullPath() ),
3982 success = false;
3983 }
3984 else
3985 {
3986 plotter->StartPlot( "1", "3D Model" );
3987 double fov_degrees = 16.5f;
3988
3989 // kind of an arbitrary distance determination
3990 float distance = sqrt( writer.GetMeshBoundingBox().SquareExtent() ) * 3;
3991
3992 std::vector<PDF_3D_VIEW> views;
3993
3994 VECTOR3D camTarget = writer.GetCenter();
3995
3996
3997 std::vector<float> c2wMatrix =
3998 PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0f, -75.0f, 25.0f );
3999
4000 views.emplace_back( PDF_3D_VIEW{
4001 .m_name = "Default",
4002 .m_cameraMatrix = c2wMatrix,
4003 .m_cameraCenter = (float) distance,
4004 .m_fov = (float) fov_degrees,
4005 } );
4006
4007
4008
4009 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0, 0.0f, 0.0f );
4010
4011 views.emplace_back( PDF_3D_VIEW{
4012 .m_name = "Top",
4013 .m_cameraMatrix = c2wMatrix,
4014 .m_cameraCenter = (float) distance,
4015 .m_fov = (float) fov_degrees,
4016 } );
4017
4018
4019
4020 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 0.0, 0.0f, 0.0f );
4021
4022 views.emplace_back( PDF_3D_VIEW{
4023 .m_name = "Bottom",
4024 .m_cameraMatrix = c2wMatrix,
4025 .m_cameraCenter = (float) distance,
4026 .m_fov = (float) fov_degrees,
4027 } );
4028
4029
4030
4031 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 90.0f, -90.0f, 90.0f );
4032
4033 views.emplace_back( PDF_3D_VIEW{
4034 .m_name = "Front",
4035 .m_cameraMatrix = c2wMatrix,
4036 .m_cameraCenter = (float) distance,
4037 .m_fov = (float) fov_degrees,
4038 } );
4039
4040 plotter->Plot3DModel( u3dTmpfn.GetFullPath(), views );
4041 plotter->EndPlot();
4042 }
4043
4044 if( success )
4045 {
4046 // Preserve the permissions of the current file
4047 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), pdfTmpfn.GetFullPath() );
4048
4049 if( !wxRenameFile( pdfTmpfn.GetFullPath(), fn.GetFullPath(), true ) )
4050 {
4051 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
4052 pdfTmpfn.GetFullPath(), fn.GetFullPath() ),
4054 success = false;
4055 }
4056 }
4057
4058 wxRemoveFile( u3dTmpfn.GetFullPath() );
4059
4060 return success;
4061}
int index
const char * name
@ ERROR_OUTSIDE
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:112
bool IsPrmSpecified(const wxString &aPrmValue)
BOARD_STACKUP_ITEM_TYPE
@ BS_ITEM_TYPE_COPPER
@ BS_ITEM_TYPE_SILKSCREEN
@ BS_ITEM_TYPE_DIELECTRIC
@ BS_ITEM_TYPE_SOLDERMASK
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
wxString GetSemanticVersion()
Get the semantic version string for KiCad defined inside the KiCadVersion.cmake file in the variable ...
const wxString & GetShortNetname() const
int GetY() const
Definition board_item.h:105
int GetX() const
Definition board_item.h:99
FOOTPRINT * GetParentFootprint() const
int GetMaxError() const
Manage one layer needed to make a physical board.
wxString GetTypeName() const
int GetSublayersCount() const
PCB_LAYER_ID GetBrdLayerId() const
int GetThickness(int aDielectricSubLayer=0) const
BOARD_STACKUP_ITEM_TYPE GetType() const
Manage layers needed to make a physical board.
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
wxString GetReferenceAsString() const
Definition footprint.h:670
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
double r
Red component.
Definition color4d.h:393
double g
Green component.
Definition color4d.h:394
COLOR4D & Darken(double aFactor)
Makes the color darker by a given factor.
Definition color4d.h:227
double a
Alpha component.
Definition color4d.h:396
double b
Blue component.
Definition color4d.h:395
PCB specific render settings.
Definition pcb_painter.h:82
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:296
Definition pad.h:55
PAD_PROP GetProperty() const
Definition pad.h:561
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition pad.h:555
bool FlashLayer(int aLayer, bool aOnlyCheckIfPermitted=false) const
Check to see whether the pad should be flashed on the specific layer.
Definition pad.cpp:410
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition pad.h:901
const VECTOR2I & GetDrillSize() const
Definition pad.h:312
PAD_ATTRIB GetAttribute() const
Definition pad.h:558
const wxString & GetNumber() const
Definition pad.h:137
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE, bool ignoreLineWidth=false) const override
Convert the pad shape to a closed polygon.
Definition pad.cpp:2481
std::shared_ptr< SHAPE_SEGMENT > GetEffectiveHoleShape() const override
Return a SHAPE_SEGMENT object representing the pad's hole.
Definition pad.cpp:1033
static std::vector< float > CreateC2WMatrixFromAngles(const VECTOR3D &aTargetPosition, float aCameraDistance, float aYawDegrees, float aPitchDegrees, float aRollDegrees)
Generates the camera to world matrix for use with a 3D View.
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:102
Definition seg.h:42
VECTOR2I A
Definition seg.h:49
VECTOR2I B
Definition seg.h:50
const VECTOR2I & GetArcMid() const
Definition shape_arc.h:120
const VECTOR2I & GetP1() const
Definition shape_arc.h:119
double GetRadius() const
const VECTOR2I & GetP0() const
Definition shape_arc.h:118
const VECTOR2I & GetCenter() const
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
const SHAPE_ARC & Arc(size_t aArc) const
bool IsClosed() const override
void SetClosed(bool aClosed)
Mark the line chain as closed (i.e.
int PointCount() const
Return the number of points (vertices) in this line chain.
ssize_t ArcIndex(size_t aSegment) const
Return the arc index for the given segment index.
void Clear()
Remove all points from the line chain.
const std::optional< INTERSECTION > SelfIntersectingWithArcs() const
Check if the line chain is self-intersecting.
int NextShape(int aPointIndex) const
Return the vertex index of the next shape in the chain, or -1 if aPointIndex is the last shape.
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
const SHAPE_LINE_CHAIN Slice(int aStartIndex, int aEndIndex) const
Return a subset of this line chain containing the [start_index, end_index] range of points.
virtual size_t GetSegmentCount() const override
const SEG CSegment(int aIndex) const
Return a constant copy of the aIndex segment in the line chain.
bool IsArcSegment(size_t aSegment) const
void RemoveShape(int aPointIndex)
Remove the shape at the given index from the line chain.
bool IsArcStart(size_t aIndex) const
Represent a set of closed polygons.
void ClearArcs()
Removes all arc references from all the outlines and holes in the polyset.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
POLYGON & Polygon(int aIndex)
Return the aIndex-th subpolygon in the set.
int FullPointCount() const
Return the number of points in the shape poly set.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
void Simplify()
Simplify the polyset (merges overlapping polys, eliminates degeneracy/self-intersections)
std::vector< SHAPE_LINE_CHAIN > POLYGON
represents a single polygon outline with holes.
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
int OutlineCount() const
Return the number of outlines in the set.
const POLYGON & CPolygon(int aIndex) const
const std::vector< POLYGON > & CPolygons() const
const SEG & GetSeg() const
int GetWidth() const override
bool MakeShapeAsThickSegment(TopoDS_Shape &aShape, const VECTOR2D &aStartPoint, const VECTOR2D &aEndPoint, double aWidth, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Make a segment shape based on start and end point.
OUTPUT_FORMAT m_outFmt
The current output format for created file.
void SetCopperColor(double r, double g, double b)
bool WritePLY(const wxString &aFileName)
bool AddCountersink(const VECTOR2I &aPosition, int aDiameter, int aDepth, int aAngle, bool aFrontSide, const VECTOR2D &aOrigin)
Add a countersink shape to remove board material from the top or bottom of a hole.
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_vias
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_pads
bool WriteSTEP(const wxString &aFileName, bool aOptimize, bool compress)
std::vector< TopoDS_Shape > m_board_back_mask
wxString m_pcbName
Name of the PCB, which will most likely be the file name of the path.
std::vector< TopoDS_Shape > m_boardCutouts
bool AddPolygonShapes(const SHAPE_POLY_SET *aPolyShapes, PCB_LAYER_ID aLayer, const VECTOR2D &aOrigin, const wxString &aNetname)
bool CreatePCB(SHAPE_POLY_SET &aOutline, const VECTOR2D &aOrigin, bool aPushBoardBody)
void getBoardBodyZPlacement(double &aZPos, double &aThickness)
TDF_Label transferModel(Handle(TDocStd_Document)&source, Handle(TDocStd_Document) &dest, const VECTOR3D &aScale)
TDF_Label m_assy_label
bool getModelLocation(bool aBottom, const VECTOR2D &aPosition, double aRotation, const VECTOR3D &aOffset, const VECTOR3D &aOrientation, TopLoc_Location &aLocation)
void SetFuseShapes(bool aValue)
bool WriteXAO(const wxString &aFileName)
bool WriteGLTF(const wxString &aFileName)
Write the assembly in binary GLTF Format.
REPORTER * m_reporter
std::vector< TDF_Label > m_pcb_labels
STEP_PCB_MODEL(const wxString &aPcbName, REPORTER *aReporter)
bool AddBackdrill(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerStart, PCB_LAYER_ID aLayerEnd, const VECTOR2D &aOrigin)
Add a backdrill hole shape to remove board material and copper plating.
void getLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
std::vector< TopoDS_Shape > m_board_outlines
void SetSimplifyShapes(bool aValue)
bool WriteU3D(const wxString &aFileName)
bool readVRML(Handle(TDocStd_Document) &aDoc, const char *aFname)
void SetPadColor(double r, double g, double b)
bool AddCounterbore(const VECTOR2I &aPosition, int aDiameter, int aDepth, bool aFrontSide, const VECTOR2D &aOrigin)
Add a counterbore shape to remove board material from the top or bottom of a hole.
void SetEnabledLayers(const LSET &aLayers)
void SetExtraPadThickness(bool aValue)
bool performMeshing(Handle(XCAFDoc_ShapeTool) &aShapeTool)
bool MakePolygonAsWall(TopoDS_Shape &aShape, SHAPE_POLY_SET &aPolySet, double aHeight, double aZposition, const VECTOR2D &aOrigin)
Make a polygonal shape to create a vertical wall.
bool readSTEP(Handle(TDocStd_Document) &aDoc, const char *aFname)
std::vector< TopoDS_Shape > m_board_front_silk
Handle(XCAFApp_Application) m_app
bool WritePDF(const wxString &aFileName)
virtual ~STEP_PCB_MODEL()
std::vector< TopoDS_Shape > m_board_front_mask
double m_copperColor[3]
bool WriteSTL(const wxString &aFileName)
void SetStackup(const BOARD_STACKUP &aStackup)
void SetNetFilter(const wxString &aFilter)
double m_padColor[3]
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper
std::map< PCB_LAYER_ID, int > GetCopperLayerKnockouts(int aDiameter, int aDepth, int aAngle, bool aFrontSide)
Get the knockout diameters for copper layers that a counterbore or countersink crosses.
bool MakeShapes(std::vector< TopoDS_Shape > &aShapes, const SHAPE_POLY_SET &aPolySet, bool aConvertToArcs, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Convert a SHAPE_POLY_SET to TopoDS_Shape's (polygonal vertical prisms, or flat faces)
bool AddBarrel(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, const wxString &aNetname)
bool readIGES(Handle(TDocStd_Document) &aDoc, const char *aFname)
bool AddHole(const SHAPE_SEGMENT &aShape, int aPlatingThickness, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, bool aCutCopper, bool aCutBody)
bool getModelLabel(const std::string &aFileNameUTF8, const VECTOR3D &aScale, TDF_Label &aLabel, bool aSubstituteModels, wxString *aErrorMessage=nullptr)
Load a 3D model data.
std::vector< TopoDS_Shape > m_copperCutouts
bool CompressSTEP(wxString &inputFile, wxString &outputFile)
std::vector< TopoDS_Shape > m_board_back_silk
void getCopperLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
std::map< wxString, std::vector< std::pair< gp_Pnt, TopoDS_Shape > > > m_pad_points
bool AddPadShape(const PAD *aPad, const VECTOR2D &aOrigin, bool aVia, SHAPE_POLY_SET *aClipPolygon=nullptr)
bool AddComponent(const std::string &aFileName, const std::string &aRefDes, bool aBottom, const VECTOR2D &aPosition, double aRotation, const VECTOR3D &aOffset, const VECTOR3D &aOrientation, const VECTOR3D &aScale, bool aSubstituteModels=true)
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_fused
void OCCSetMergeMaxDistance(double aDistance=OCC_MAX_DISTANCE_TO_MERGE_POINTS)
BOARD_STACKUP m_stackup
bool WriteBREP(const wxString &aFileName)
bool Perform(const Handle(TDocStd_Document) &aDocument)
Definition writer.cpp:967
const VECTOR3D & GetCenter() const
Definition writer.h:209
const Bnd_Box & GetMeshBoundingBox() const
Definition writer.h:210
wxString StringFromValue(double aValue, bool aAddUnitLabel=false, EDA_DATA_TYPE aType=EDA_DATA_TYPE::DISTANCE) const
Converts aValue in internal units into a united string.
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:283
void TransformCircleToPolygon(SHAPE_LINE_CHAIN &aBuffer, const VECTOR2I &aCenter, int aRadius, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a circle to a polygon, using multiple straight lines.
void TransformOvalToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aStart, const VECTOR2I &aEnd, int aWidth, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a oblong shape to a polygon, using multiple segments.
#define _(s)
static constexpr EDA_ANGLE ANGLE_360
Definition eda_angle.h:417
const wxChar *const traceKiCad2Step
Flag to enable KiCad2Step debug tracing.
Handle(KICAD3D_INFO) KICAD3D_INFO
wxString LayerName(int aLayer)
Returns the default display name for a given layer.
Definition layer_id.cpp:31
bool IsFrontLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a front layer.
Definition layer_ids.h:780
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:803
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:677
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ B_Mask
Definition layer_ids.h:98
@ B_Cu
Definition layer_ids.h:65
@ F_Mask
Definition layer_ids.h:97
@ F_SilkS
Definition layer_ids.h:100
@ B_SilkS
Definition layer_ids.h:101
@ F_Cu
Definition layer_ids.h:64
This file contains miscellaneous commonly used macros and functions.
bool DuplicatePermissions(const wxString &aSrc, const wxString &aDest)
Duplicates the file security data from one file to another ensuring that they are the same between bo...
Definition unix/io.cpp:47
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ PTH
Plated through hole pad.
Definition padstack.h:98
@ CASTELLATED
a pad with a castellated through hole
Definition padstack.h:121
#define _HKI(x)
Definition page_info.cpp:44
#define USER_PREC
Plotting engines similar to ps (PostScript, Gerber, svg)
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_DEBUG
static bool addSegment(VRML_LAYER &model, IDF_SEGMENT *seg, int icont, int iseg)
const std::vector< FAB_LAYER_COLOR > & GetStandardColors(BOARD_STACKUP_ITEM_TYPE aType)
wxString NotSpecifiedPrm()
#define KEY_PREPREG
#define KEY_CORE
MODEL3D_FORMAT_TYPE
@ FMT_STEP
@ FMT_IGES
@ FMT_IDF
@ FMT_WRZ
@ FMT_STEPZ
@ FMT_NONE
@ FMT_WRL
@ FMT_EMN
static TopoDS_Shape fuseShapesOrCompound(const TopTools_ListOfShape &aInputShapes, REPORTER *aReporter)
static bool colorFromStackup(BOARD_STACKUP_ITEM_TYPE aType, const wxString &aColorStr, COLOR4D &aColorOut)
static bool makeWireFromChain(BRepLib_MakeWire &aMkWire, const SHAPE_LINE_CHAIN &aChain, double aMergeOCCMaxDist, double aZposition, const VECTOR2D &aOrigin, REPORTER *aReporter)
static Standard_Boolean prefixNames(const TDF_Label &aLabel, const TCollection_ExtendedString &aPrefix)
#define APPROX_DBG(stmt)
static Standard_Boolean rescaleShapes(const TDF_Label &theLabel, const gp_XYZ &aScale)
static constexpr double BOARD_OFFSET
static wxString formatBBox(const BOX2I &aBBox)
static bool fuseShapes(auto &aInputShapes, TopoDS_Shape &aOutShape, REPORTER *aReporter)
static TopoDS_Compound makeCompound(const auto &aInputShapes)
static std::vector< FAB_LAYER_COLOR > s_soldermaskColors
MODEL3D_FORMAT_TYPE fileType(const char *aFileName)
static VECTOR2D CircleCenterFrom3Points(const VECTOR2D &p1, const VECTOR2D &p2, const VECTOR2D &p3)
static SHAPE_LINE_CHAIN approximateLineChainWithArcs(const SHAPE_LINE_CHAIN &aSrc)
static constexpr double USER_ANGLE_PREC
static TopoDS_Shape getOneShape(Handle(XCAFDoc_ShapeTool) aShapeTool)
static constexpr double OCC_MAX_DISTANCE_TO_MERGE_POINTS
Default distance between points to treat them as separate ones (mm) 0.001 mm or less is a reasonable ...
std::pair< std::string, TDF_Label > MODEL_DATUM
#define CLOSE_STREAM(var)
#define OPEN_ISTREAM(var, name)
wxString UnescapeString(const wxString &aSource)
wxString GetISO8601CurrentDateTime()
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
KIBIS top(path, &reporter)
VECTOR2I center
int radius
VECTOR2I end
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
#define M_PI
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:31
wxLogTrace helper definitions.
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:229
double DEG2RAD(double deg)
Definition trigo.h:166
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695
VECTOR2< double > VECTOR2D
Definition vector2d.h:694
VECTOR3< double > VECTOR3D
Definition vector3.h:230