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