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