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