KiCad PCB EDA Suite
Loading...
Searching...
No Matches
exporter_step.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2022 Mark Roszko <[email protected]>
5 * Copyright (C) 2016 Cirilo Bernardo <[email protected]>
6 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, you may find one here:
20 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21 * or you may search the http://www.gnu.org website for the version 2 license,
22 * or you may write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26#include "exporter_step.h"
27#include <advanced_config.h>
28#include <board.h>
31#include <footprint.h>
32#include <pcb_textbox.h>
33#include <pcb_table.h>
34#include <pcb_tablecell.h>
35#include <pcb_track.h>
36#include <pcb_shape.h>
37#include <pcb_barcode.h>
38#include <pcb_painter.h>
39#include <pad.h>
40#include <zone.h>
42#include "step_pcb_model.h"
43
44#include <pgm_base.h>
45#include <reporter.h>
46#include <base_units.h>
47#include <filename_resolver.h>
48#include <trace_helpers.h>
49#include <project_pcb.h>
51
52#include <new> // std::bad_alloc
53#include <Message.hxx> // OpenCascade messenger
54#include <Message_PrinterOStream.hxx> // OpenCascade output messenger
55#include <Standard_Failure.hxx> // In open cascade
56
57#include <Standard_Version.hxx>
58
59#include <wx/crt.h>
60#include <wx/log.h>
61#include <core/profile.h> // To use GetRunningMicroSecs or another profiling utility
62
63#define OCC_VERSION_MIN 0x070500
64
65#if OCC_VERSION_HEX < OCC_VERSION_MIN
66#include <Message_Messenger.hxx>
67#endif
68
69
70class KICAD_PRINTER : public Message_Printer
71{
72public:
73 KICAD_PRINTER( REPORTER* aReporter ) :
74 m_reporter( aReporter )
75 {}
76
77protected:
78#if OCC_VERSION_HEX < OCC_VERSION_MIN
79 virtual void Send( const TCollection_ExtendedString& theString,
80 const Message_Gravity theGravity,
81 const Standard_Boolean theToPutEol ) const override
82 {
83 Send( TCollection_AsciiString( theString ), theGravity, theToPutEol );
84 }
85
86 virtual void Send( const TCollection_AsciiString& theString,
87 const Message_Gravity theGravity,
88 const Standard_Boolean theToPutEol ) const override
89#else
90 virtual void send( const TCollection_AsciiString& theString,
91 const Message_Gravity theGravity ) const override
92#endif
93 {
94 wxString msg( theString.ToCString() );
95
96#if OCC_VERSION_HEX < OCC_VERSION_MIN
97 if( theToPutEol )
98 msg += wxT( "\n" );
99#else
100 msg += wxT( "\n" );
101#endif
102
103 m_reporter->Report( msg, getSeverity( theGravity ) );
104 }
105
106private:
107 SEVERITY getSeverity( const Message_Gravity theGravity ) const
108 {
109 switch( theGravity )
110 {
111 case Message_Trace: return RPT_SEVERITY_DEBUG;
112 case Message_Info: return RPT_SEVERITY_DEBUG;
113 case Message_Warning: return RPT_SEVERITY_WARNING;
114 case Message_Alarm: return RPT_SEVERITY_WARNING;
115 case Message_Fail: return RPT_SEVERITY_ERROR;
116
117 // There are no other values, but gcc doesn't appear to be able to work that out.
118 default: return RPT_SEVERITY_UNDEFINED;
119 }
120 }
121
122private:
124};
125
126
128 REPORTER* aReporter ) :
129 m_params( aParams ),
130 m_reporter( aReporter ),
131 m_board( aBoard ),
132 m_pcbModel( nullptr )
133{
134 m_copperColor = COLOR4D( 0.7, 0.61, 0.0, 1.0 );
135
136 if( m_params.m_ExportComponents )
137 m_padColor = COLOR4D( 0.50, 0.50, 0.50, 1.0 );
138 else
140
141 // TODO: make configurable
142 m_platingThickness = pcbIUScale.mmToIU( 0.025 );
143
144 // Init m_pcbBaseName to the board short filename (no path, no ext)
145 // m_pcbName is used later to identify items in step file
146 wxFileName fn( aBoard->GetFileName() );
147 m_pcbBaseName = fn.GetName();
148
149 m_resolver = std::make_unique<FILENAME_RESOLVER>();
150 m_resolver->Set3DConfigDir( wxT( "" ) );
151 // needed to add the project to the search stack
152 m_resolver->SetProject( aBoard->GetProject() );
153 m_resolver->SetProgramBase( &Pgm() );
154}
155
156
160
161
163 PCB_LAYER_ID aEndLayer ) const
164{
165 if( !IsCopperLayer( aLayer ) )
166 return false;
167
168 // Quick check for exact match
169 if( aLayer == aStartLayer || aLayer == aEndLayer )
170 return true;
171
172 // Convert layers to a sortable index for comparison
173 // F_Cu = -1, In1_Cu through In30_Cu = 0-29, B_Cu = MAX_CU_LAYERS (32)
174 auto layerToIndex = []( PCB_LAYER_ID layer ) -> int
175 {
176 if( layer == F_Cu )
177 return -1;
178
179 if( layer == B_Cu )
180 return MAX_CU_LAYERS;
181
182 if( IsInnerCopperLayer( layer ) )
183 return layer - In1_Cu;
184
185 return -2; // Invalid copper layer
186 };
187
188 int startIdx = layerToIndex( aStartLayer );
189 int endIdx = layerToIndex( aEndLayer );
190 int layerIdx = layerToIndex( aLayer );
191
192 if( layerIdx == -2 )
193 return false;
194
195 int minIdx = std::min( startIdx, endIdx );
196 int maxIdx = std::max( startIdx, endIdx );
197
198 return ( layerIdx >= minIdx && layerIdx <= maxIdx );
199}
200
201
203 SHAPE_POLY_SET* aClipPolygon )
204{
205 bool hasdata = false;
206 std::vector<PAD*> padsMatchingNetFilter;
207
208 // Dump the pad holes into the PCB
209 for( PAD* pad : aFootprint->Pads() )
210 {
211 bool castellated = pad->GetProperty() == PAD_PROP::CASTELLATED;
212 std::shared_ptr<SHAPE_SEGMENT> holeShape = pad->GetEffectiveHoleShape();
213
214 SHAPE_POLY_SET holePoly;
215 holeShape->TransformToPolygon( holePoly, pad->GetMaxError(), ERROR_INSIDE );
216
217 // This helps with fusing
218 holePoly.Deflate( m_platingThickness / 2, CORNER_STRATEGY::ROUND_ALL_CORNERS, pad->GetMaxError() );
219
220 for( PCB_LAYER_ID pcblayer : pad->GetLayerSet() )
221 {
222 if( pad->IsOnLayer( pcblayer ) )
223 m_poly_holes[pcblayer].Append( holePoly );
224 }
225
226 if( pad->HasHole() )
227 {
228 int platingThickness = pad->GetAttribute() == PAD_ATTRIB::PTH ? m_platingThickness : 0;
229
230 if( m_pcbModel->AddHole( *holeShape, platingThickness, F_Cu, B_Cu, false, aOrigin, true, true ) )
231 hasdata = true;
232
234 //if( m_layersToExport.Contains( F_SilkS ) || m_layersToExport.Contains( B_SilkS ) )
235 //{
236 // m_poly_holes[F_SilkS].Append( holePoly );
237 // m_poly_holes[B_SilkS].Append( holePoly );
238 //}
239
240 // Handle backdrills - secondary and tertiary drills defined in the padstack
241 const PADSTACK& padstack = pad->Padstack();
242 const PADSTACK::DRILL_PROPS& secondaryDrill = padstack.SecondaryDrill();
243 const PADSTACK::DRILL_PROPS& tertiaryDrill = padstack.TertiaryDrill();
244
245 // Process secondary drill (typically bottom backdrill)
246 if( secondaryDrill.size.x > 0 )
247 {
248 SHAPE_SEGMENT backdrillShape( pad->GetPosition(), pad->GetPosition(),
249 secondaryDrill.size.x );
250 m_pcbModel->AddBackdrill( backdrillShape, secondaryDrill.start,
251 secondaryDrill.end, aOrigin );
252
253 // Add backdrill holes to affected copper layers for 2D polygon subtraction
254 SHAPE_POLY_SET backdrillPoly;
255 backdrillShape.TransformToPolygon( backdrillPoly, pad->GetMaxError(), ERROR_INSIDE );
256
257 for( PCB_LAYER_ID layer : pad->GetLayerSet() )
258 {
259 if( isLayerInBackdrillSpan( layer, secondaryDrill.start, secondaryDrill.end ) )
260 m_poly_holes[layer].Append( backdrillPoly );
261 }
262
263 // Add knockouts for silkscreen and soldermask on the backdrill side
264 if( isLayerInBackdrillSpan( F_Cu, secondaryDrill.start, secondaryDrill.end ) )
265 {
266 m_poly_holes[F_SilkS].Append( backdrillPoly );
267 m_poly_holes[F_Mask].Append( backdrillPoly );
268 }
269 if( isLayerInBackdrillSpan( B_Cu, secondaryDrill.start, secondaryDrill.end ) )
270 {
271 m_poly_holes[B_SilkS].Append( backdrillPoly );
272 m_poly_holes[B_Mask].Append( backdrillPoly );
273 }
274 }
275
276 // Process tertiary drill (typically top backdrill)
277 if( tertiaryDrill.size.x > 0 )
278 {
279 SHAPE_SEGMENT backdrillShape( pad->GetPosition(), pad->GetPosition(),
280 tertiaryDrill.size.x );
281 m_pcbModel->AddBackdrill( backdrillShape, tertiaryDrill.start,
282 tertiaryDrill.end, aOrigin );
283
284 // Add backdrill holes to affected copper layers for 2D polygon subtraction
285 SHAPE_POLY_SET backdrillPoly;
286 backdrillShape.TransformToPolygon( backdrillPoly, pad->GetMaxError(), ERROR_INSIDE );
287
288 for( PCB_LAYER_ID layer : pad->GetLayerSet() )
289 {
290 if( isLayerInBackdrillSpan( layer, tertiaryDrill.start, tertiaryDrill.end ) )
291 m_poly_holes[layer].Append( backdrillPoly );
292 }
293
294 // Add knockouts for silkscreen and soldermask on the backdrill side
295 if( isLayerInBackdrillSpan( F_Cu, tertiaryDrill.start, tertiaryDrill.end ) )
296 {
297 m_poly_holes[F_SilkS].Append( backdrillPoly );
298 m_poly_holes[F_Mask].Append( backdrillPoly );
299 }
300 if( isLayerInBackdrillSpan( B_Cu, tertiaryDrill.start, tertiaryDrill.end ) )
301 {
302 m_poly_holes[B_SilkS].Append( backdrillPoly );
303 m_poly_holes[B_Mask].Append( backdrillPoly );
304 }
305 }
306
307 // Process post-machining (counterbore/countersink) on front and back
308 const PADSTACK::POST_MACHINING_PROPS& frontPM = padstack.FrontPostMachining();
309 const PADSTACK::POST_MACHINING_PROPS& backPM = padstack.BackPostMachining();
310
311 wxLogTrace( traceKiCad2Step, wxT( "PAD post-machining check: frontPM.mode.has_value=%d frontPM.size=%d frontPM.depth=%d frontPM.angle=%d" ),
312 frontPM.mode.has_value() ? 1 : 0, frontPM.size, frontPM.depth, frontPM.angle );
313 wxLogTrace( traceKiCad2Step, wxT( "PAD post-machining check: backPM.mode.has_value=%d backPM.size=%d backPM.depth=%d backPM.angle=%d" ),
314 backPM.mode.has_value() ? 1 : 0, backPM.size, backPM.depth, backPM.angle );
315
316 // For counterbore, depth must be > 0. For countersink, depth can be 0 (calculated from diameter/angle)
317 bool frontPMValid = frontPM.mode.has_value() && frontPM.size > 0 &&
318 ( ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE && frontPM.depth > 0 ) ||
319 ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK && frontPM.angle > 0 ) );
320
321 if( frontPMValid )
322 {
323 wxLogTrace( traceKiCad2Step, wxT( "PAD front post-machining: mode=%d (COUNTERBORE=2, COUNTERSINK=3)" ),
324 static_cast<int>( *frontPM.mode ) );
325
326 int pmAngle = ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) ? frontPM.angle : 0;
327
329 {
330 m_pcbModel->AddCounterbore( pad->GetPosition(), frontPM.size,
331 frontPM.depth, true, aOrigin );
332 }
334 {
335 m_pcbModel->AddCountersink( pad->GetPosition(), frontPM.size,
336 frontPM.depth, frontPM.angle, true, aOrigin );
337 }
338
339 // Add knockouts to all copper layers the feature crosses
340 auto knockouts = m_pcbModel->GetCopperLayerKnockouts( frontPM.size, frontPM.depth,
341 pmAngle, true );
342 for( const auto& [layer, diameter] : knockouts )
343 {
344 SHAPE_POLY_SET pmPoly;
345 TransformCircleToPolygon( pmPoly, pad->GetPosition(), diameter / 2,
346 pad->GetMaxError(), ERROR_INSIDE );
347 m_poly_holes[layer].Append( pmPoly );
348 }
349
350 // Add knockout for silkscreen and soldermask on front side (full diameter)
351 SHAPE_POLY_SET pmPoly;
352 TransformCircleToPolygon( pmPoly, pad->GetPosition(), frontPM.size / 2,
353 pad->GetMaxError(), ERROR_INSIDE );
354 m_poly_holes[F_SilkS].Append( pmPoly );
355 m_poly_holes[F_Mask].Append( pmPoly );
356 }
357
358 // For counterbore, depth must be > 0. For countersink, depth can be 0 (calculated from diameter/angle)
359 bool backPMValid = backPM.mode.has_value() && backPM.size > 0 &&
360 ( ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE && backPM.depth > 0 ) ||
361 ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK && backPM.angle > 0 ) );
362
363 if( backPMValid )
364 {
365 wxLogTrace( traceKiCad2Step, wxT( "PAD back post-machining: mode=%d (COUNTERBORE=2, COUNTERSINK=3)" ),
366 static_cast<int>( *backPM.mode ) );
367
368 int pmAngle = ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) ? backPM.angle : 0;
369
371 {
372 m_pcbModel->AddCounterbore( pad->GetPosition(), backPM.size,
373 backPM.depth, false, aOrigin );
374 }
376 {
377 m_pcbModel->AddCountersink( pad->GetPosition(), backPM.size,
378 backPM.depth, backPM.angle, false, aOrigin );
379 }
380
381 // Add knockouts to all copper layers the feature crosses
382 auto knockouts = m_pcbModel->GetCopperLayerKnockouts( backPM.size, backPM.depth,
383 pmAngle, false );
384 for( const auto& [layer, diameter] : knockouts )
385 {
386 SHAPE_POLY_SET pmPoly;
387 TransformCircleToPolygon( pmPoly, pad->GetPosition(), diameter / 2,
388 pad->GetMaxError(), ERROR_INSIDE );
389 m_poly_holes[layer].Append( pmPoly );
390 }
391
392 // Add knockout for silkscreen and soldermask on back side (full diameter)
393 SHAPE_POLY_SET pmPoly;
394 TransformCircleToPolygon( pmPoly, pad->GetPosition(), backPM.size / 2,
395 pad->GetMaxError(), ERROR_INSIDE );
396 m_poly_holes[B_SilkS].Append( pmPoly );
397 m_poly_holes[B_Mask].Append( pmPoly );
398 }
399 }
400
401 if( !m_params.m_NetFilter.IsEmpty() && !pad->GetNetname().Matches( m_params.m_NetFilter ) )
402 continue;
403
404 if( m_params.m_ExportPads )
405 {
406 if( m_pcbModel->AddPadShape( pad, aOrigin, false, castellated ? aClipPolygon : nullptr) )
407 hasdata = true;
408
409 if( m_params.m_ExportSoldermask )
410 {
411 for( PCB_LAYER_ID pcblayer : pad->GetLayerSet() )
412 {
413 if( pcblayer != F_Mask && pcblayer != B_Mask )
414 continue;
415
416 SHAPE_POLY_SET poly;
417 PCB_LAYER_ID cuLayer = ( pcblayer == F_Mask ) ? F_Cu : B_Cu;
418 pad->TransformShapeToPolygon( poly, cuLayer, pad->GetSolderMaskExpansion( cuLayer ),
419 pad->GetMaxError(), ERROR_INSIDE );
420
421 m_poly_shapes[pcblayer][wxEmptyString].Append( poly );
422 }
423 }
424 }
425
426 padsMatchingNetFilter.push_back( pad );
427 }
428
429 // Build 3D shapes of the footprint graphic items:
430 for( PCB_LAYER_ID pcblayer : m_layersToExport )
431 {
432 if( IsCopperLayer( pcblayer ) && !m_params.m_ExportTracksVias )
433 continue;
434
435 SHAPE_POLY_SET buffer;
436
437 aFootprint->TransformFPShapesToPolySet( buffer, pcblayer, 0, aFootprint->GetMaxError(), ERROR_INSIDE,
438 true, /* include text */
439 true, /* include shapes */
440 false /* include private items */ );
441
442 if( !IsCopperLayer( pcblayer ) )
443 {
444 m_poly_shapes[pcblayer][wxEmptyString].Append( buffer );
445 }
446 else
447 {
448 std::map<const SHAPE_POLY_SET::POLYGON*, PAD*> polyPadMap;
449
450 // Only add polygons colliding with any matching pads
451 for( const SHAPE_POLY_SET::POLYGON& poly : buffer.CPolygons() )
452 {
453 for( PAD* pad : padsMatchingNetFilter )
454 {
455 if( !pad->IsOnLayer( pcblayer ) )
456 continue;
457
458 std::shared_ptr<SHAPE_POLY_SET> padPoly = pad->GetEffectivePolygon( pcblayer );
459 SHAPE_POLY_SET gfxPoly( poly );
460
461 if( padPoly->Collide( &gfxPoly ) )
462 {
463 polyPadMap[&poly] = pad;
464 m_poly_shapes[pcblayer][pad->GetNetname()].Append( gfxPoly );
465 break;
466 }
467 }
468 }
469
470 if( m_params.m_NetFilter.empty() )
471 {
472 // Add polygons with no net
473 for( const SHAPE_POLY_SET::POLYGON& poly : buffer.CPolygons() )
474 {
475 auto it = polyPadMap.find( &poly );
476
477 if( it == polyPadMap.end() )
478 m_poly_shapes[pcblayer][wxEmptyString].Append( poly );
479 }
480 }
481 }
482 }
483
484 if( ( !(aFootprint->GetAttributes() & (FP_THROUGH_HOLE|FP_SMD)) ) && !m_params.m_IncludeUnspecified )
485 {
486 return hasdata;
487 }
488
489 if( aFootprint->GetDNPForVariant( m_board ? m_board->GetCurrentVariant() : wxString() )
490 && !m_params.m_IncludeDNP )
491 {
492 return hasdata;
493 }
494
495 // Prefetch the library for this footprint
496 // In case we need to resolve relative footprint paths
497 wxString libraryName = aFootprint->GetFPID().GetLibNickname();
498 wxString footprintBasePath = wxEmptyString;
499
500 double posX = aFootprint->GetPosition().x - aOrigin.x;
501 double posY = (aFootprint->GetPosition().y) - aOrigin.y;
502
503 if( m_board->GetProject() )
504 {
505 std::optional<LIBRARY_TABLE_ROW*> fpRow =
506 PROJECT_PCB::FootprintLibAdapter( m_board->GetProject() )->GetRow( libraryName );
507 if( fpRow )
508 footprintBasePath = LIBRARY_MANAGER::GetFullURI( *fpRow, true );
509 }
510
511 // Exit early if we don't want to include footprint models
512 if( m_params.m_BoardOnly || !m_params.m_ExportComponents )
513 {
514 return hasdata;
515 }
516
517 bool componentFilter = !m_params.m_ComponentFilter.IsEmpty();
518 std::vector<wxString> componentFilterPatterns;
519
520 if( componentFilter )
521 {
522 wxStringTokenizer tokenizer( m_params.m_ComponentFilter, ", \t\r\n", wxTOKEN_STRTOK );
523
524 while( tokenizer.HasMoreTokens() )
525 componentFilterPatterns.push_back( tokenizer.GetNextToken() );
526
527 bool found = false;
528
529 for( const wxString& pattern : componentFilterPatterns )
530 {
531 if( aFootprint->GetReference().Matches( pattern ) )
532 {
533 found = true;
534 break;
535 }
536 }
537
538 if( !found )
539 return hasdata;
540 }
541
542 VECTOR2D newpos( pcbIUScale.IUTomm( posX ), pcbIUScale.IUTomm( posY ) );
543
544 for( const FP_3DMODEL& fp_model : aFootprint->Models() )
545 {
546 if( !fp_model.m_Show || fp_model.m_Filename.empty() )
547 continue;
548
549 std::vector<wxString> searchedPaths;
550 std::vector<const EMBEDDED_FILES*> embeddedFilesStack;
551 embeddedFilesStack.push_back( aFootprint->GetEmbeddedFiles() );
552 embeddedFilesStack.push_back( m_board->GetEmbeddedFiles() );
553
554 wxString mainPath = m_resolver->ResolvePath( fp_model.m_Filename, footprintBasePath,
555 embeddedFilesStack );
556
557 if( mainPath.empty() || !wxFileName::FileExists( mainPath ) )
558 {
559 // the error path will return an empty name sometimes, at least report back the original filename
560 if( mainPath.empty() )
561 mainPath = fp_model.m_Filename;
562
563 m_reporter->Report( wxString::Format( _( "Could not add 3D model for %s.\n"
564 "File not found: %s\n" ),
565 aFootprint->GetReference(), mainPath ),
567 continue;
568 }
569
570 wxString baseName =
571 fp_model.m_Filename.AfterLast( '/' ).AfterLast( '\\' ).BeforeLast( '.' );
572
573 std::vector<wxString> altFilenames;
574
575 // Add embedded files to alternative filenames
576 if( fp_model.m_Filename.StartsWith( FILEEXT::KiCadUriPrefix + "://" ) )
577 {
578 for( const EMBEDDED_FILES* filesPtr : embeddedFilesStack )
579 {
580 const auto& map = filesPtr->EmbeddedFileMap();
581
582 for( auto& [fname, file] : map )
583 {
584 if( fname.BeforeLast( '.' ) == baseName )
585 {
586 wxFileName temp_file = filesPtr->GetTemporaryFileName( fname );
587
588 if( !temp_file.IsOk() )
589 continue;
590
591 wxString altPath = temp_file.GetFullPath();
592
593 if( mainPath == altPath )
594 continue;
595
596 altFilenames.emplace_back( altPath );
597 }
598 }
599 }
600 }
601
602 try
603 {
604 bool bottomSide = aFootprint->GetLayer() == B_Cu;
605
606 // the rotation is stored in degrees but opencascade wants radians
607 VECTOR3D modelRot = fp_model.m_Rotation;
608 modelRot *= M_PI;
609 modelRot /= 180.0;
610
611 if( m_pcbModel->AddComponent(
612 baseName, mainPath, altFilenames, aFootprint->GetReference(), bottomSide,
613 newpos, aFootprint->GetOrientation().AsRadians(), fp_model.m_Offset,
614 modelRot, fp_model.m_Scale, m_params.m_SubstModels ) )
615 {
616 hasdata = true;
617 }
618 }
619 catch( const Standard_Failure& e )
620 {
621 m_reporter->Report( wxString::Format( _( "Could not add 3D model for %s.\n"
622 "OpenCASCADE error: %s\n" ),
623 aFootprint->GetReference(),
624 e.GetMessageString() ),
626 }
627
628 }
629
630 return hasdata;
631}
632
633
635{
636 bool skipCopper = !m_params.m_ExportTracksVias
637 || ( !m_params.m_NetFilter.IsEmpty()
638 && !aTrack->GetNetname().Matches( m_params.m_NetFilter ) );
639
640 if( m_params.m_ExportSoldermask && aTrack->IsOnLayer( F_Mask ) )
641 {
642 aTrack->TransformShapeToPolygon( m_poly_shapes[F_Mask][wxEmptyString], F_Mask,
643 aTrack->GetSolderMaskExpansion(), aTrack->GetMaxError(),
644 ERROR_INSIDE );
645 }
646
647 if( m_params.m_ExportSoldermask && aTrack->IsOnLayer( B_Mask ) )
648 {
649 aTrack->TransformShapeToPolygon( m_poly_shapes[B_Mask][wxEmptyString], B_Mask,
650 aTrack->GetSolderMaskExpansion(), aTrack->GetMaxError(),
651 ERROR_INSIDE );
652 }
653
654 if( aTrack->Type() == PCB_VIA_T )
655 {
656 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
657
658 std::shared_ptr<SHAPE_SEGMENT> holeShape = via->GetEffectiveHoleShape();
659 SHAPE_POLY_SET holePoly;
660 holeShape->TransformToPolygon( holePoly, via->GetMaxError(), ERROR_INSIDE );
661
662 // This helps with fusing
663 holePoly.Deflate( m_platingThickness / 2, CORNER_STRATEGY::ROUND_ALL_CORNERS, via->GetMaxError() );
664
665 LSET layers( via->GetLayerSet() & m_layersToExport );
666
667 PCB_LAYER_ID top_layer, bot_layer;
668 via->LayerPair( &top_layer, &bot_layer );
669
670 if( !skipCopper )
671 {
672 for( PCB_LAYER_ID pcblayer : layers )
673 {
674 const std::shared_ptr<SHAPE>& shape = via->GetEffectiveShape( pcblayer );
675
676 SHAPE_POLY_SET poly;
677 shape->TransformToPolygon( poly, via->GetMaxError(), ERROR_INSIDE );
678 m_poly_shapes[pcblayer][via->GetNetname()].Append( poly );
679 m_poly_holes[pcblayer].Append( holePoly );
680 }
681
682 m_pcbModel->AddBarrel( *holeShape, top_layer, bot_layer, true, aOrigin, via->GetNetname() );
683 }
684
686 //if( m_layersToExport.Contains( F_SilkS ) || m_layersToExport.Contains( B_SilkS ) )
687 //{
688 // m_poly_holes[F_SilkS].Append( holePoly );
689 // m_poly_holes[B_SilkS].Append( holePoly );
690 //}
691
692 // Cut via holes in soldermask when the via is not tented.
693 // This ensures the mask has a proper hole through the via drill, not just the annular ring opening.
694 if( m_params.m_ExportSoldermask )
695 {
696 if( via->IsOnLayer( F_Mask ) )
697 m_poly_holes[F_Mask].Append( holePoly );
698
699 if( via->IsOnLayer( B_Mask ) )
700 m_poly_holes[B_Mask].Append( holePoly );
701 }
702
703 m_pcbModel->AddHole( *holeShape, m_platingThickness, top_layer, bot_layer, true, aOrigin,
704 !m_params.m_FillAllVias, m_params.m_CutViasInBody );
705
706 // Handle via backdrills - secondary and tertiary drills defined in the padstack
707 const PADSTACK& padstack = via->Padstack();
708 const PADSTACK::DRILL_PROPS& secondaryDrill = padstack.SecondaryDrill();
709 const PADSTACK::DRILL_PROPS& tertiaryDrill = padstack.TertiaryDrill();
710
711 // Process secondary drill (typically bottom backdrill)
712 if( secondaryDrill.size.x > 0 )
713 {
714 SHAPE_SEGMENT backdrillShape( via->GetPosition(), via->GetPosition(),
715 secondaryDrill.size.x );
716 m_pcbModel->AddBackdrill( backdrillShape, secondaryDrill.start,
717 secondaryDrill.end, aOrigin );
718
719 // Add backdrill holes to affected copper layers for 2D polygon subtraction
720 SHAPE_POLY_SET backdrillPoly;
721 backdrillShape.TransformToPolygon( backdrillPoly, via->GetMaxError(), ERROR_INSIDE );
722
723 for( PCB_LAYER_ID layer : via->GetLayerSet() )
724 {
725 if( isLayerInBackdrillSpan( layer, secondaryDrill.start, secondaryDrill.end ) )
726 m_poly_holes[layer].Append( backdrillPoly );
727 }
728
729 // Add knockouts for silkscreen and soldermask on the backdrill side
730 if( isLayerInBackdrillSpan( F_Cu, secondaryDrill.start, secondaryDrill.end ) )
731 {
732 m_poly_holes[F_SilkS].Append( backdrillPoly );
733 m_poly_holes[F_Mask].Append( backdrillPoly );
734 }
735 if( isLayerInBackdrillSpan( B_Cu, secondaryDrill.start, secondaryDrill.end ) )
736 {
737 m_poly_holes[B_SilkS].Append( backdrillPoly );
738 m_poly_holes[B_Mask].Append( backdrillPoly );
739 }
740 }
741
742 // Process tertiary drill (typically top backdrill)
743 if( tertiaryDrill.size.x > 0 )
744 {
745 SHAPE_SEGMENT backdrillShape( via->GetPosition(), via->GetPosition(),
746 tertiaryDrill.size.x );
747 m_pcbModel->AddBackdrill( backdrillShape, tertiaryDrill.start,
748 tertiaryDrill.end, aOrigin );
749
750 // Add backdrill holes to affected copper layers for 2D polygon subtraction
751 SHAPE_POLY_SET backdrillPoly;
752 backdrillShape.TransformToPolygon( backdrillPoly, via->GetMaxError(), ERROR_INSIDE );
753
754 for( PCB_LAYER_ID layer : via->GetLayerSet() )
755 {
756 if( isLayerInBackdrillSpan( layer, tertiaryDrill.start, tertiaryDrill.end ) )
757 m_poly_holes[layer].Append( backdrillPoly );
758 }
759
760 // Add knockouts for silkscreen and soldermask on the backdrill side
761 if( isLayerInBackdrillSpan( F_Cu, tertiaryDrill.start, tertiaryDrill.end ) )
762 {
763 m_poly_holes[F_SilkS].Append( backdrillPoly );
764 m_poly_holes[F_Mask].Append( backdrillPoly );
765 }
766 if( isLayerInBackdrillSpan( B_Cu, tertiaryDrill.start, tertiaryDrill.end ) )
767 {
768 m_poly_holes[B_SilkS].Append( backdrillPoly );
769 m_poly_holes[B_Mask].Append( backdrillPoly );
770 }
771 }
772
773 // Process post-machining (counterbore/countersink) on front and back
774 const PADSTACK::POST_MACHINING_PROPS& frontPM = padstack.FrontPostMachining();
775 const PADSTACK::POST_MACHINING_PROPS& backPM = padstack.BackPostMachining();
776
777 wxLogTrace( traceKiCad2Step, wxT( "VIA post-machining check: frontPM.mode.has_value=%d frontPM.size=%d frontPM.depth=%d frontPM.angle=%d" ),
778 frontPM.mode.has_value() ? 1 : 0, frontPM.size, frontPM.depth, frontPM.angle );
779 wxLogTrace( traceKiCad2Step, wxT( "VIA post-machining check: backPM.mode.has_value=%d backPM.size=%d backPM.depth=%d backPM.angle=%d" ),
780 backPM.mode.has_value() ? 1 : 0, backPM.size, backPM.depth, backPM.angle );
781
782 // For counterbore, depth must be > 0. For countersink, depth can be 0 (calculated from diameter/angle)
783 bool frontPMValid = frontPM.mode.has_value() && frontPM.size > 0 &&
784 ( ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE && frontPM.depth > 0 ) ||
785 ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK && frontPM.angle > 0 ) );
786
787 if( frontPMValid )
788 {
789 wxLogTrace( traceKiCad2Step, wxT( "VIA front post-machining: mode=%d (COUNTERBORE=2, COUNTERSINK=3)" ),
790 static_cast<int>( *frontPM.mode ) );
791
792 int pmAngle = ( *frontPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) ? frontPM.angle : 0;
793
795 {
796 m_pcbModel->AddCounterbore( via->GetPosition(), frontPM.size,
797 frontPM.depth, true, aOrigin );
798 }
800 {
801 m_pcbModel->AddCountersink( via->GetPosition(), frontPM.size,
802 frontPM.depth, frontPM.angle, true, aOrigin );
803 }
804
805 // Add knockouts to all copper layers the feature crosses
806 auto knockouts = m_pcbModel->GetCopperLayerKnockouts( frontPM.size, frontPM.depth,
807 pmAngle, true );
808 for( const auto& [layer, diameter] : knockouts )
809 {
810 SHAPE_POLY_SET pmPoly;
811 TransformCircleToPolygon( pmPoly, via->GetPosition(), diameter / 2,
812 via->GetMaxError(), ERROR_INSIDE );
813 m_poly_holes[layer].Append( pmPoly );
814 }
815
816 // Add knockout for silkscreen and soldermask on front side (full diameter)
817 SHAPE_POLY_SET pmPoly;
818 TransformCircleToPolygon( pmPoly, via->GetPosition(), frontPM.size / 2,
819 via->GetMaxError(), ERROR_INSIDE );
820 m_poly_holes[F_SilkS].Append( pmPoly );
821 m_poly_holes[F_Mask].Append( pmPoly );
822 }
823
824 // For counterbore, depth must be > 0. For countersink, depth can be 0 (calculated from diameter/angle)
825 bool backPMValid = backPM.mode.has_value() && backPM.size > 0 &&
826 ( ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE && backPM.depth > 0 ) ||
827 ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK && backPM.angle > 0 ) );
828
829 if( backPMValid )
830 {
831 wxLogTrace( traceKiCad2Step, wxT( "VIA back post-machining: mode=%d (COUNTERBORE=2, COUNTERSINK=3)" ),
832 static_cast<int>( *backPM.mode ) );
833
834 int pmAngle = ( *backPM.mode == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) ? backPM.angle : 0;
835
837 {
838 m_pcbModel->AddCounterbore( via->GetPosition(), backPM.size,
839 backPM.depth, false, aOrigin );
840 }
842 {
843 m_pcbModel->AddCountersink( via->GetPosition(), backPM.size,
844 backPM.depth, backPM.angle, false, aOrigin );
845 }
846
847 // Add knockouts to all copper layers the feature crosses
848 auto knockouts = m_pcbModel->GetCopperLayerKnockouts( backPM.size, backPM.depth,
849 pmAngle, false );
850 for( const auto& [layer, diameter] : knockouts )
851 {
852 SHAPE_POLY_SET pmPoly;
853 TransformCircleToPolygon( pmPoly, via->GetPosition(), diameter / 2,
854 via->GetMaxError(), ERROR_INSIDE );
855 m_poly_holes[layer].Append( pmPoly );
856 }
857
858 // Add knockout for silkscreen and soldermask on back side (full diameter)
859 SHAPE_POLY_SET pmPoly;
860 TransformCircleToPolygon( pmPoly, via->GetPosition(), backPM.size / 2,
861 via->GetMaxError(), ERROR_INSIDE );
862 m_poly_holes[B_SilkS].Append( pmPoly );
863 m_poly_holes[B_Mask].Append( pmPoly );
864 }
865
866 return true;
867 }
868
869 if( skipCopper )
870 return true;
871
872 PCB_LAYER_ID pcblayer = aTrack->GetLayer();
873
874 if( !m_layersToExport.Contains( pcblayer ) )
875 return false;
876
877 aTrack->TransformShapeToPolygon( m_poly_shapes[pcblayer][aTrack->GetNetname()], pcblayer, 0,
878 aTrack->GetMaxError(), ERROR_INSIDE );
879
880 return true;
881}
882
883
884void EXPORTER_STEP::buildZones3DShape( VECTOR2D aOrigin, bool aSolderMaskOnly )
885{
886 for( ZONE* zone : m_board->Zones() )
887 {
888 LSET layers = zone->GetLayerSet();
889
890 // Filter by net if a net filter is specified and zone is on copper layer(s)
891 if( ( layers & LSET::AllCuMask() ).count() && !m_params.m_NetFilter.IsEmpty()
892 && !zone->GetNetname().Matches( m_params.m_NetFilter ) )
893 {
894 continue;
895 }
896
897 for( PCB_LAYER_ID layer : layers )
898 {
899 bool isMaskLayer = ( layer == F_Mask || layer == B_Mask );
900
901 // If we're only processing soldermask zones, skip non-mask layers
902 if( aSolderMaskOnly && !isMaskLayer )
903 continue;
904
905 // If we're doing full zone export, skip mask layers if they'll be handled separately
906 if( !aSolderMaskOnly && isMaskLayer && !m_params.m_ExportZones )
907 continue;
908
909 SHAPE_POLY_SET fill_shape;
910 zone->TransformSolidAreasShapesToPolygon( layer, fill_shape );
911 fill_shape.Unfracture();
912
913 fill_shape.SimplifyOutlines( ADVANCED_CFG::GetCfg().m_TriangulateSimplificationLevel );
914
915 m_poly_shapes[layer][zone->GetNetname()].Append( fill_shape );
916 }
917 }
918}
919
920
922{
923 PCB_LAYER_ID pcblayer = aItem->GetLayer();
924 int maxError = aItem->GetMaxError();
925
926 if( !m_layersToExport.Contains( pcblayer ) )
927 return false;
928
929 if( IsCopperLayer( pcblayer ) && !m_params.m_ExportTracksVias )
930 return false;
931
932 if( IsInnerCopperLayer( pcblayer ) && !m_params.m_ExportInnerCopper )
933 return false;
934
935 switch( aItem->Type() )
936 {
937 case PCB_SHAPE_T:
938 {
939 PCB_SHAPE* graphic = static_cast<PCB_SHAPE*>( aItem );
940
941 if( IsCopperLayer( pcblayer ) && !m_params.m_NetFilter.IsEmpty()
942 && !graphic->GetNetname().Matches( m_params.m_NetFilter ) )
943 {
944 return true;
945 }
946
947 LINE_STYLE lineStyle = graphic->GetLineStyle();
948
949 if( lineStyle == LINE_STYLE::SOLID )
950 {
951 graphic->TransformShapeToPolySet( m_poly_shapes[pcblayer][graphic->GetNetname()], pcblayer, 0,
952 maxError, ERROR_INSIDE );
953 }
954 else
955 {
956 std::vector<SHAPE*> shapes = graphic->MakeEffectiveShapes( true );
957 const PCB_PLOT_PARAMS& plotParams = m_board->GetPlotOptions();
958 KIGFX::PCB_RENDER_SETTINGS renderSettings;
959
960 renderSettings.SetDashLengthRatio( plotParams.GetDashedLineDashRatio() );
961 renderSettings.SetGapLengthRatio( plotParams.GetDashedLineGapRatio() );
962
963 for( SHAPE* shape : shapes )
964 {
965 STROKE_PARAMS::Stroke( shape, lineStyle, graphic->GetWidth(), &renderSettings,
966 [&]( const VECTOR2I& a, const VECTOR2I& b )
967 {
968 SHAPE_SEGMENT seg( a, b, graphic->GetWidth() );
969 seg.TransformToPolygon( m_poly_shapes[pcblayer][graphic->GetNetname()],
970 maxError, ERROR_INSIDE );
971 } );
972 }
973
974 for( SHAPE* shape : shapes )
975 delete shape;
976 }
977
978 if( graphic->IsHatchedFill() )
979 m_poly_shapes[pcblayer][graphic->GetNetname()].Append( graphic->GetHatching() );
980
981 if( m_params.m_ExportSoldermask && graphic->IsOnLayer( F_Mask ) )
982 {
983 graphic->TransformShapeToPolygon( m_poly_shapes[F_Mask][wxEmptyString], F_Mask,
984 graphic->GetSolderMaskExpansion(), maxError, ERROR_INSIDE );
985 }
986
987 if( m_params.m_ExportSoldermask && graphic->IsOnLayer( B_Mask ) )
988 {
989 graphic->TransformShapeToPolygon( m_poly_shapes[B_Mask][wxEmptyString], B_Mask,
990 graphic->GetSolderMaskExpansion(), maxError, ERROR_INSIDE );
991 }
992
993 break;
994 }
995
996 case PCB_TEXT_T:
997 {
998 PCB_TEXT* text = static_cast<PCB_TEXT*>( aItem );
999
1000 text->TransformTextToPolySet( m_poly_shapes[pcblayer][wxEmptyString], 0, maxError, ERROR_INSIDE );
1001 break;
1002 }
1003
1004 case PCB_BARCODE_T:
1005 {
1006 PCB_BARCODE* barcode = static_cast<PCB_BARCODE*>( aItem );
1007
1008 barcode->TransformShapeToPolySet( m_poly_shapes[pcblayer][wxEmptyString], pcblayer, 0, maxError,
1009 ERROR_INSIDE );
1010 break;
1011 }
1012
1013 case PCB_TEXTBOX_T:
1014 {
1015 PCB_TEXTBOX* textbox = static_cast<PCB_TEXTBOX*>( aItem );
1016
1017 // border
1018 if( textbox->IsBorderEnabled() )
1019 {
1020 textbox->PCB_SHAPE::TransformShapeToPolygon( m_poly_shapes[pcblayer][wxEmptyString], pcblayer, 0,
1021 maxError, ERROR_INSIDE );
1022 }
1023
1024 // text
1025 textbox->TransformTextToPolySet( m_poly_shapes[pcblayer][wxEmptyString], 0, maxError, ERROR_INSIDE );
1026 break;
1027 }
1028
1029 case PCB_TABLE_T:
1030 {
1031 PCB_TABLE* table = static_cast<PCB_TABLE*>( aItem );
1032
1033 for( PCB_TABLECELL* cell : table->GetCells() )
1034 {
1035 cell->TransformTextToPolySet( m_poly_shapes[pcblayer][wxEmptyString], 0, maxError, ERROR_INSIDE );
1036 }
1037
1038 table->DrawBorders(
1039 [&]( const VECTOR2I& ptA, const VECTOR2I& ptB, const STROKE_PARAMS& stroke )
1040 {
1041 SHAPE_SEGMENT seg( ptA, ptB, stroke.GetWidth() );
1042 seg.TransformToPolygon( m_poly_shapes[pcblayer][wxEmptyString], maxError, ERROR_INSIDE );
1043 } );
1044
1045 break;
1046 }
1047
1048 default: wxFAIL_MSG( "buildGraphic3DShape: unhandled item type" );
1049 }
1050
1051 return true;
1052}
1053
1054
1056{
1057 // Specialize the STEP_PCB_MODEL generator for specific output format
1058 // it can have some minor actions for the generator
1059 switch( m_params.m_Format )
1060 {
1062 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_STEP );
1063 break;
1064
1066 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_STEPZ );
1067 break;
1068
1070 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_BREP );
1071 break;
1072
1074 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_XAO );
1075 break;
1076
1078 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_GLTF );
1079 break;
1080
1082 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_PLY );
1083 break;
1084
1086 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_STL );
1087 break;
1088
1090 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_U3D );
1091 break;
1092
1094 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_PDF );
1095 break;
1096
1097 default:
1098 m_pcbModel->SpecializeVariant( OUTPUT_FORMAT::FMT_OUT_UNKNOWN );
1099 break;
1100 }
1101}
1102
1103
1105{
1106 if( m_pcbModel )
1107 return true;
1108
1109 SHAPE_POLY_SET pcbOutlines; // stores the board main outlines
1110
1111 if( !m_board->GetBoardPolygonOutlines( pcbOutlines,
1112 /* infer outline if necessary */ true,
1113 /* error handler */ nullptr,
1114 /* allows use arcs in outlines */ true ) )
1115 {
1116 wxLogWarning( _( "Board outline is malformed. Run DRC for a full analysis." ) );
1117 }
1118
1119 SHAPE_POLY_SET pcbOutlinesNoArcs = pcbOutlines;
1120 pcbOutlinesNoArcs.ClearArcs();
1121
1122 VECTOR2D origin;
1123
1124 // Determine the coordinate system reference:
1125 // Precedence of reference point is Drill Origin > Grid Origin > User Offset
1126 if( m_params.m_UseDrillOrigin )
1127 origin = m_board->GetDesignSettings().GetAuxOrigin();
1128 else if( m_params.m_UseGridOrigin )
1129 origin = m_board->GetDesignSettings().GetGridOrigin();
1130 else
1131 origin = m_params.m_Origin;
1132
1133 m_pcbModel = std::make_unique<STEP_PCB_MODEL>( m_pcbBaseName, m_reporter );
1134
1136
1137 m_pcbModel->SetCopperColor( m_copperColor.r, m_copperColor.g, m_copperColor.b );
1138 m_pcbModel->SetPadColor( m_padColor.r, m_padColor.g, m_padColor.b );
1139
1140 m_pcbModel->SetStackup( m_board->GetStackupOrDefault() );
1141 m_pcbModel->SetEnabledLayers( m_layersToExport );
1142 m_pcbModel->SetFuseShapes( m_params.m_FuseShapes );
1143 m_pcbModel->SetNetFilter( m_params.m_NetFilter );
1144 m_pcbModel->SetExtraPadThickness( m_params.m_ExtraPadThickness );
1145
1146 // Note: m_params.m_BoardOutlinesChainingEpsilon is used only to build the board outlines,
1147 // not to set OCC chaining epsilon (much smaller)
1148 //
1149 // Set the min distance between 2 points for OCC to see these 2 points as merged
1150 // OCC_MAX_DISTANCE_TO_MERGE_POINTS is acceptable for OCC, otherwise there are issues
1151 // to handle the shapes chaining on copper layers, because the Z dist is 0.035 mm and the
1152 // min dist must be much smaller (we use 0.001 mm giving good results)
1153 m_pcbModel->OCCSetMergeMaxDistance( OCC_MAX_DISTANCE_TO_MERGE_POINTS );
1154
1155 // For copper layers, only pads and tracks are added, because adding everything on copper
1156 // generate unreasonable file sizes and take a unreasonable calculation time.
1157 for( FOOTPRINT* fp : m_board->Footprints() )
1158 buildFootprint3DShapes( fp, origin, &pcbOutlinesNoArcs );
1159
1160 for( PCB_TRACK* track : m_board->Tracks() )
1161 buildTrack3DShape( track, origin );
1162
1163 for( BOARD_ITEM* item : m_board->Drawings() )
1164 buildGraphic3DShape( item, origin );
1165
1166 if( m_params.m_ExportZones )
1167 buildZones3DShape( origin );
1168
1169 // Process zones on soldermask layers even when copper zone export is disabled.
1170 // This ensures mask openings defined by zones are properly exported.
1171 if( m_params.m_ExportSoldermask && !m_params.m_ExportZones )
1172 buildZones3DShape( origin, true );
1173
1174 for( PCB_LAYER_ID pcblayer : m_layersToExport.Seq() )
1175 {
1176 SHAPE_POLY_SET holes = m_poly_holes[pcblayer];
1177 holes.Simplify();
1178
1179 if( pcblayer == F_Mask || pcblayer == B_Mask )
1180 {
1181 // Mask layer is negative
1182 SHAPE_POLY_SET mask = pcbOutlinesNoArcs;
1183
1184 for( auto& [netname, poly] : m_poly_shapes[pcblayer] )
1185 {
1186 poly.Simplify();
1187
1188 poly.SimplifyOutlines( pcbIUScale.mmToIU( 0.003 ) );
1189 poly.Simplify();
1190
1191 mask.BooleanSubtract( poly );
1192 }
1193
1194 mask.BooleanSubtract( holes );
1195
1196 m_pcbModel->AddPolygonShapes( &mask, pcblayer, origin, wxEmptyString );
1197 }
1198 else
1199 {
1200 for( auto& [netname, poly] : m_poly_shapes[pcblayer] )
1201 {
1202 poly.Simplify();
1203
1204 poly.SimplifyOutlines( pcbIUScale.mmToIU( 0.003 ) );
1205 poly.Simplify();
1206
1207 // Subtract holes
1208 poly.BooleanSubtract( holes );
1209
1210 // Clip to board outline
1211 poly.BooleanIntersection( pcbOutlinesNoArcs );
1212
1213 m_pcbModel->AddPolygonShapes( &poly, pcblayer, origin, netname );
1214 }
1215 }
1216 }
1217
1218 m_reporter->Report( wxT( "Create PCB solid model.\n" ), RPT_SEVERITY_DEBUG );
1219
1220 m_reporter->Report( wxString::Format( wxT( "Board outline: found %d initial points.\n" ),
1221 pcbOutlines.FullPointCount() ),
1223
1224 if( !m_pcbModel->CreatePCB( pcbOutlines, origin, m_params.m_ExportBoardBody ) )
1225 {
1226 m_reporter->Report( _( "Could not create PCB solid model.\n" ), RPT_SEVERITY_ERROR );
1227 return false;
1228 }
1229
1230 return true;
1231}
1232
1233
1235{
1236 // Display the export time, for statistics
1237 int64_t stats_startExportTime = GetRunningMicroSecs();
1238
1239 // setup opencascade message log
1240 struct SCOPED_PRINTER
1241 {
1242 Handle( Message_Printer ) m_handle;
1243
1244 SCOPED_PRINTER( const Handle( Message_Printer ) & aHandle ) : m_handle( aHandle )
1245 {
1246 Message::DefaultMessenger()->AddPrinter( m_handle );
1247 };
1248
1249 ~SCOPED_PRINTER() { Message::DefaultMessenger()->RemovePrinter( m_handle ); }
1250 };
1251
1252 Message::DefaultMessenger()->RemovePrinters( STANDARD_TYPE( Message_PrinterOStream ) );
1253 SCOPED_PRINTER occtPrinter( new KICAD_PRINTER( m_reporter ) );
1254
1255 m_reporter->Report( wxT( "Determining PCB data.\n" ), RPT_SEVERITY_DEBUG );
1256
1257 if( m_params.m_OutputFile.IsEmpty() )
1258 {
1259 wxFileName fn = m_board->GetFileName();
1260 fn.SetName( fn.GetName() );
1261 fn.SetExt( m_params.GetDefaultExportExtension() );
1262
1263 m_params.m_OutputFile = fn.GetFullName();
1264 }
1265
1267
1268 if( m_params.m_ExportInnerCopper )
1270
1271 if( m_params.m_ExportSilkscreen )
1272 {
1275 }
1276
1277 if( m_params.m_ExportSoldermask )
1278 {
1279 m_layersToExport.set( F_Mask );
1280 m_layersToExport.set( B_Mask );
1281 }
1282
1283 m_layersToExport &= m_board->GetEnabledLayers();
1284
1285 try
1286 {
1287 m_reporter->Report( wxString::Format( wxT( "Build %s data.\n" ), m_params.GetFormatName() ),
1289
1290 if( !buildBoard3DShapes() )
1291 {
1292 m_reporter->Report( _( "\n"
1293 "** Error building STEP board model. Export aborted. **\n" ),
1295 return false;
1296 }
1297
1298 m_reporter->Report( wxString::Format( wxT( "Writing %s file.\n" ), m_params.GetFormatName() ),
1300
1301 bool success = true;
1303 success = m_pcbModel->WriteSTEP( m_outputFile, m_params.m_OptimizeStep, false );
1304 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::STEPZ )
1305 success = m_pcbModel->WriteSTEP( m_outputFile, m_params.m_OptimizeStep, true );
1306 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::BREP )
1307 success = m_pcbModel->WriteBREP( m_outputFile );
1308 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::XAO )
1309 success = m_pcbModel->WriteXAO( m_outputFile );
1310 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::GLB )
1311 success = m_pcbModel->WriteGLTF( m_outputFile );
1312 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::PLY )
1313 success = m_pcbModel->WritePLY( m_outputFile );
1314 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::STL )
1315 success = m_pcbModel->WriteSTL( m_outputFile );
1316 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::U3D )
1317 success = m_pcbModel->WriteU3D( m_outputFile );
1318 else if( m_params.m_Format == EXPORTER_STEP_PARAMS::FORMAT::PDF )
1319 success = m_pcbModel->WritePDF( m_outputFile );
1320
1321 if( !success )
1322 {
1323 m_reporter->Report( wxString::Format( _( "\n"
1324 "** Error writing %s file. **\n" ),
1325 m_params.GetFormatName() ),
1327 return false;
1328 }
1329 else
1330 {
1331 m_reporter->Report( wxString::Format( wxT( "%s file '%s' created.\n" ),
1332 m_params.GetFormatName(),
1333 m_outputFile ),
1335 }
1336 }
1337 catch( const std::bad_alloc& )
1338 {
1339 m_reporter->Report( wxString::Format( _( "\n** Out of memory while exporting %s file. **\n"
1340 "The board may have too many objects (e.g., vias, tracks, components) "
1341 "to process with available system memory.\n"
1342 "Try disabling 'Fuse Shapes' option, reducing board complexity, "
1343 "or freeing up system memory.\n" ),
1344 m_params.GetFormatName() ),
1346 return false;
1347 }
1348 catch( const Standard_Failure& e )
1349 {
1350 wxString errorMsg = e.GetMessageString();
1351 m_reporter->Report( wxString::Format( _( "\nOpenCASCADE error: %s\n" ), errorMsg ),
1353
1354 // Check if this might be memory-related based on common OCC error patterns
1355 if( errorMsg.Contains( "alloc" ) || errorMsg.Contains( "memory" ) ||
1356 errorMsg.IsEmpty() )
1357 {
1358 m_reporter->Report( _( "This error may indicate insufficient memory. Consider disabling "
1359 "'Fuse Shapes', reducing the number of vias/components, or freeing "
1360 "system memory.\n" ),
1362 }
1363
1364 m_reporter->Report( wxString::Format( _( "** Error exporting %s file. Export aborted. **\n" ),
1365 m_params.GetFormatName() ),
1367 return false;
1368 }
1369 #ifndef DEBUG
1370 catch( ... )
1371 {
1372 m_reporter->Report( wxString::Format( _( "\n** Unexpected error while exporting %s file. **\n"
1373 "This may be caused by insufficient system memory, especially "
1374 "when exporting boards with many vias or components with 'Fuse Shapes' enabled.\n"
1375 "Try disabling 'Fuse Shapes', reducing board complexity, "
1376 "or freeing up system memory.\n" ),
1377 m_params.GetFormatName() ),
1379 return false;
1380 }
1381 #endif
1382
1383 // Display calculation time in seconds
1384 double calculation_time = (double)( GetRunningMicroSecs() - stats_startExportTime) / 1e6;
1385 m_reporter->Report( wxString::Format( _( "\n"
1386 "Export time %.3f s\n" ),
1387 calculation_time ),
1389
1390 return !m_reporter->HasMessageOfSeverity( RPT_SEVERITY_ERROR );
1391}
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:112
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
Definition board_item.h:236
virtual void TransformShapeToPolySet(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, KIGFX::RENDER_SETTINGS *aRenderSettings=nullptr) const
Convert the item shape to a polyset.
Definition board_item.h:429
int GetMaxError() const
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
const wxString & GetFileName() const
Definition board.h:359
PROJECT * GetProject() const
Definition board.h:579
double AsRadians() const
Definition eda_angle.h:120
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
const SHAPE_POLY_SET & GetHatching() const
Definition eda_shape.h:148
virtual std::vector< SHAPE * > MakeEffectiveShapes(bool aEdgeOnly=false) const
Make a set of SHAPE objects representing the EDA_SHAPE.
Definition eda_shape.h:379
bool IsHatchedFill() const
Definition eda_shape.h:124
LINE_STYLE GetLineStyle() const
REPORTER * m_reporter
bool buildTrack3DShape(PCB_TRACK *aTrack, const VECTOR2D &aOrigin)
bool buildFootprint3DShapes(FOOTPRINT *aFootprint, const VECTOR2D &aOrigin, SHAPE_POLY_SET *aClipPolygon)
std::map< PCB_LAYER_ID, SHAPE_POLY_SET > m_poly_holes
bool isLayerInBackdrillSpan(PCB_LAYER_ID aLayer, PCB_LAYER_ID aStartLayer, PCB_LAYER_ID aEndLayer) const
Check if a copper layer is within a backdrill layer span (inclusive).
wxString m_outputFile
EXPORTER_STEP_PARAMS m_params
wxString m_pcbBaseName
the name of the project (board short filename (no path, no ext) used to identify items in step file
std::unique_ptr< FILENAME_RESOLVER > m_resolver
std::unique_ptr< STEP_PCB_MODEL > m_pcbModel
std::map< PCB_LAYER_ID, std::map< wxString, SHAPE_POLY_SET > > m_poly_shapes
KIGFX::COLOR4D m_copperColor
EXPORTER_STEP(BOARD *aBoard, const EXPORTER_STEP_PARAMS &aParams, REPORTER *aReporter)
bool buildGraphic3DShape(BOARD_ITEM *aItem, const VECTOR2D &aOrigin)
KIGFX::COLOR4D m_padColor
void buildZones3DShape(VECTOR2D aOrigin, bool aSolderMaskOnly=false)
EDA_ANGLE GetOrientation() const
Definition footprint.h:330
std::deque< PAD * > & Pads()
Definition footprint.h:306
int GetAttributes() const
Definition footprint.h:417
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition footprint.h:339
void TransformFPShapesToPolySet(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool aIncludeText=true, bool aIncludeShapes=true, bool aIncludePrivateItems=false) const
Generate shapes of graphic items (outlines) on layer aLayer as polygons and adds these polygons to aB...
const LIB_ID & GetFPID() const
Definition footprint.h:351
bool GetDNPForVariant(const wxString &aVariantName) const
Get the DNP status for a specific variant.
std::vector< FP_3DMODEL > & Models()
Definition footprint.h:323
const wxString & GetReference() const
Definition footprint.h:751
EMBEDDED_FILES * GetEmbeddedFiles() override
Definition footprint.h:1203
VECTOR2I GetPosition() const override
Definition footprint.h:327
virtual void Send(const TCollection_ExtendedString &theString, const Message_Gravity theGravity, const Standard_Boolean theToPutEol) const override
REPORTER * m_reporter
SEVERITY getSeverity(const Message_Gravity theGravity) const
KICAD_PRINTER(REPORTER *aReporter)
virtual void Send(const TCollection_AsciiString &theString, const Message_Gravity theGravity, const Standard_Boolean theToPutEol) const override
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
PCB specific render settings.
Definition pcb_painter.h:82
void SetGapLengthRatio(double aRatio)
void SetDashLengthRatio(double aRatio)
std::optional< LIBRARY_TABLE_ROW * > GetRow(const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH) const
Like LIBRARY_MANAGER::GetRow but filtered to the LIBRARY_TABLE_TYPE of this adapter.
std::optional< wxString > GetFullURI(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, bool aSubstituted=false) const
Return the full location specifying URI for the LIB, either in original UI form or in environment var...
const UTF8 & GetLibNickname() const
Return the logical library name portion of a LIB_ID.
Definition lib_id.h:87
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & ExternalCuMask()
Return a mask holding the Front and Bottom layers.
Definition lset.cpp:634
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:599
static const LSET & InternalCuMask()
Return a complete set of internal copper layers which is all Cu layers except F_Cu and B_Cu.
Definition lset.cpp:577
A PADSTACK defines the characteristics of a single or multi-layer pad, in the IPC sense of the word.
Definition padstack.h:157
POST_MACHINING_PROPS & FrontPostMachining()
Definition padstack.h:357
DRILL_PROPS & TertiaryDrill()
Definition padstack.h:354
DRILL_PROPS & SecondaryDrill()
Definition padstack.h:351
POST_MACHINING_PROPS & BackPostMachining()
Definition padstack.h:360
Definition pad.h:55
Parameters and options when plotting/printing a board.
double GetDashedLineGapRatio() const
double GetDashedLineDashRatio() const
int GetWidth() const override
int GetSolderMaskExpansion() const
void TransformShapeToPolySet(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, KIGFX::RENDER_SETTINGS *aRenderSettings=nullptr) const override
Convert the item shape to a polyset.
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth=false) const override
Convert the shape to a closed polygon.
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
bool IsBorderEnabled() const
Disables the border, this is done by changing the stroke internally.
void TransformTextToPolySet(SHAPE_POLY_SET &aBuffer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc) const
Function TransformTextToPolySet Convert the text to a polygonSet describing the actual character stro...
int GetSolderMaskExpansion() const
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth=false) const override
Convert the track shape to a closed polygon.
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
static FOOTPRINT_LIBRARY_ADAPTER * FootprintLibAdapter(PROJECT *aProject)
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:73
Represent a set of closed polygons.
void ClearArcs()
Removes all arc references from all the outlines and holes in the polyset.
int FullPointCount() const
Return the number of points in the shape poly set.
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 Unfracture()
Convert a single outline slitted ("fractured") polygon into a set ouf outlines with holes.
void SimplifyOutlines(int aMaxError=0)
Simplifies the lines in the polyset.
void Deflate(int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError)
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const std::vector< POLYGON > & CPolygons() const
void TransformToPolygon(SHAPE_POLY_SET &aBuffer, int aError, ERROR_LOC aErrorLoc) const override
Fills a SHAPE_POLY_SET with a polygon representation of this shape.
An abstract shape on 2D plane.
Definition shape.h:126
Simple container to manage line stroke parameters.
int GetWidth() const
static void Stroke(const SHAPE *aShape, LINE_STYLE aLineStyle, int aWidth, const KIGFX::RENDER_SETTINGS *aRenderSettings, const std::function< void(const VECTOR2I &a, const VECTOR2I &b)> &aStroker)
Handle a list of polygons defining a copper zone.
Definition zone.h:74
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.
@ ROUND_ALL_CORNERS
All angles are rounded.
#define _(s)
@ FP_SMD
Definition footprint.h:85
@ FP_THROUGH_HOLE
Definition footprint.h:84
static const std::string KiCadUriPrefix
const wxChar *const traceKiCad2Step
Flag to enable KiCad2Step debug tracing.
Handle(KICAD3D_INFO) KICAD3D_INFO
#define MAX_CU_LAYERS
Definition layer_ids.h:176
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:677
bool IsInnerCopperLayer(int aLayerId)
Test whether a layer is an inner (In1_Cu to In30_Cu) copper layer.
Definition layer_ids.h:699
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ B_Mask
Definition layer_ids.h:98
@ B_Cu
Definition layer_ids.h:65
@ F_Mask
Definition layer_ids.h:97
@ F_SilkS
Definition layer_ids.h:100
@ In1_Cu
Definition layer_ids.h:66
@ B_SilkS
Definition layer_ids.h:101
@ F_Cu
Definition layer_ids.h:64
@ PTH
Plated through hole pad.
Definition padstack.h:98
@ CASTELLATED
a pad with a castellated through hole
Definition padstack.h:121
BARCODE class definition.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
int64_t GetRunningMicroSecs()
An alternate way to calculate an elapsed time (in microsecondes) to class PROF_COUNTER.
SEVERITY
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_UNDEFINED
@ RPT_SEVERITY_DEBUG
@ RPT_SEVERITY_INFO
@ RPT_SEVERITY_ACTION
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 ...
LINE_STYLE
Dashed line types.
! The properties of a padstack drill. Drill position is always the pad position (origin).
Definition padstack.h:266
PCB_LAYER_ID start
Definition padstack.h:269
PCB_LAYER_ID end
Definition padstack.h:270
VECTOR2I size
Drill diameter (x == y) or slot dimensions (x != y)
Definition padstack.h:267
std::optional< PAD_DRILL_POST_MACHINING_MODE > mode
Definition padstack.h:281
#define M_PI
wxLogTrace helper definitions.
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:88
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:97
@ PCB_TEXTBOX_T
class PCB_TEXTBOX, wrapped text on a layer
Definition typeinfo.h:93
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:92
@ PCB_BARCODE_T
class PCB_BARCODE, a barcode (graphic item)
Definition typeinfo.h:101
@ PCB_TABLE_T
class PCB_TABLE, table of PCB_TABLECELLs
Definition typeinfo.h:94
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695
VECTOR2< double > VECTOR2D
Definition vector2d.h:694
VECTOR3< double > VECTOR3D
Definition vector3.h:230
Definition of file extensions used in Kicad.