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