/** * @file plot_board_layers.cpp * @brief Functions to plot one board layer (silkscreen layers or other layers). * Silkscreen layers have specific requirement for pads (not filled) and texts * (with option to remove them from some copper areas (pads...) */ /* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 1992-2020 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include // for KiROUND #include #include #include #include #include #include #include #include #include #include #include #include /* * Plot a solder mask layer. Solder mask layers have a minimum thickness value and cannot be * drawn like standard layers, unless the minimum thickness is 0. */ static void PlotSolderMaskLayer( BOARD *aBoard, PLOTTER* aPlotter, LSET aLayerMask, const PCB_PLOT_PARAMS& aPlotOpt, int aMinThickness ); /* * Creates the plot for silkscreen layers. Silkscreen layers have specific requirement for * pads (not filled) and texts (with option to remove them from some copper areas (pads...) */ void PlotSilkScreen( BOARD *aBoard, PLOTTER* aPlotter, LSET aLayerMask, const PCB_PLOT_PARAMS& aPlotOpt ) { BRDITEMS_PLOTTER itemplotter( aPlotter, aBoard, aPlotOpt ); itemplotter.SetLayerSet( aLayerMask ); // Plot edge layer and graphic items itemplotter.PlotBoardGraphicItems(); // Plot footprint outlines : itemplotter.Plot_Edges_Modules(); // Plot pads (creates pads outlines, for pads on silkscreen layers) LSET layersmask_plotpads = aLayerMask; // Calculate the mask layers of allowed layers for pads if( !aPlotOpt.GetPlotPadsOnSilkLayer() ) // Do not plot pads on silk screen layers layersmask_plotpads.set( B_SilkS, false ).set( F_SilkS, false ); if( layersmask_plotpads.any() ) { for( auto Module : aBoard->Modules() ) { aPlotter->StartBlock( NULL ); for( auto pad : Module->Pads() ) { // See if the pad is on this layer LSET masklayer = pad->GetLayerSet(); if( !( masklayer & layersmask_plotpads ).any() ) continue; COLOR4D color = COLOR4D::BLACK; if( layersmask_plotpads[B_SilkS] ) color = aPlotter->ColorSettings()->GetColor( B_SilkS ); if( layersmask_plotpads[F_SilkS] ) { color = ( color == COLOR4D::BLACK ) ? aPlotter->ColorSettings()->GetColor( F_SilkS ) : color; } itemplotter.PlotPad( pad, color, SKETCH ); } aPlotter->EndBlock( NULL ); } } // Plot footprints fields (ref, value ...) for( auto module : aBoard->Modules() ) { if( ! itemplotter.PlotAllTextsModule( module ) ) { wxLogMessage( _( "Your BOARD has a bad layer number for footprint %s" ), module->GetReference() ); } } // Plot filled areas aPlotter->StartBlock( NULL ); // Plot all zones together so we don't end up with divots where zones touch each other. ZONE_CONTAINER* zone = nullptr; SHAPE_POLY_SET aggregateArea; for( ZONE_CONTAINER* candidate : aBoard->Zones() ) { if( !aLayerMask[ candidate->GetLayer() ] ) continue; if( !zone ) zone = candidate; aggregateArea.BooleanAdd( candidate->GetFilledPolysList(), SHAPE_POLY_SET::PM_FAST ); } if( zone ) { aggregateArea.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); itemplotter.PlotFilledAreas( zone, aggregateArea ); } aPlotter->EndBlock( NULL ); } void PlotOneBoardLayer( BOARD *aBoard, PLOTTER* aPlotter, PCB_LAYER_ID aLayer, const PCB_PLOT_PARAMS& aPlotOpt ) { PCB_PLOT_PARAMS plotOpt = aPlotOpt; int soldermask_min_thickness = aBoard->GetDesignSettings().m_SolderMaskMinWidth; // Set a default color and the text mode for this layer aPlotter->SetColor( aPlotOpt.GetColor() ); aPlotter->SetTextMode( aPlotOpt.GetTextMode() ); // Specify that the contents of the "Edges Pcb" layer are to be plotted in addition to the // contents of the currently specified layer. LSET layer_mask( aLayer ); if( !aPlotOpt.GetExcludeEdgeLayer() ) layer_mask.set( Edge_Cuts ); if( IsCopperLayer( aLayer ) ) { // Skip NPTH pads on copper layers ( only if hole size == pad size ): // Drill mark will be plotted if drill mark is SMALL_DRILL_SHAPE or FULL_DRILL_SHAPE if( plotOpt.GetFormat() == PLOT_FORMAT::DXF ) { plotOpt.SetSkipPlotNPTH_Pads( false ); PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); } else { plotOpt.SetSkipPlotNPTH_Pads( true ); PlotStandardLayer( aBoard, aPlotter, layer_mask, plotOpt ); } } else { switch( aLayer ) { case B_Mask: case F_Mask: plotOpt.SetSkipPlotNPTH_Pads( false ); // Disable plot pad holes plotOpt.SetDrillMarksType( PCB_PLOT_PARAMS::NO_DRILL_SHAPE ); // Plot solder mask: if( soldermask_min_thickness == 0 ) { if( plotOpt.GetFormat() == PLOT_FORMAT::DXF ) PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); else PlotStandardLayer( aBoard, aPlotter, layer_mask, plotOpt ); } else PlotSolderMaskLayer( aBoard, aPlotter, layer_mask, plotOpt, soldermask_min_thickness ); break; case B_Adhes: case F_Adhes: case B_Paste: case F_Paste: plotOpt.SetSkipPlotNPTH_Pads( false ); // Disable plot pad holes plotOpt.SetDrillMarksType( PCB_PLOT_PARAMS::NO_DRILL_SHAPE ); if( plotOpt.GetFormat() == PLOT_FORMAT::DXF ) PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); else PlotStandardLayer( aBoard, aPlotter, layer_mask, plotOpt ); break; case F_SilkS: case B_SilkS: if( plotOpt.GetFormat() == PLOT_FORMAT::DXF && plotOpt.GetDXFPlotPolygonMode() ) // PlotLayerOutlines() is designed only for DXF plotters. // and must not be used for other plot formats PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); else PlotSilkScreen( aBoard, aPlotter, layer_mask, plotOpt ); // Gerber: Subtract soldermask from silkscreen if enabled if( aPlotter->GetPlotterType() == PLOT_FORMAT::GERBER && plotOpt.GetSubtractMaskFromSilk() ) { if( aLayer == F_SilkS ) layer_mask = LSET( F_Mask ); else layer_mask = LSET( B_Mask ); // Create the mask to subtract by creating a negative layer polarity aPlotter->SetLayerPolarity( false ); // Disable plot pad holes plotOpt.SetDrillMarksType( PCB_PLOT_PARAMS::NO_DRILL_SHAPE ); // Plot the mask PlotStandardLayer( aBoard, aPlotter, layer_mask, plotOpt ); } break; // These layers are plotted like silk screen layers. // Mainly, pads on these layers are not filled. // This is not necessary the best choice. case Dwgs_User: case Cmts_User: case Eco1_User: case Eco2_User: case Edge_Cuts: case Margin: case F_CrtYd: case B_CrtYd: case F_Fab: case B_Fab: plotOpt.SetSkipPlotNPTH_Pads( false ); plotOpt.SetDrillMarksType( PCB_PLOT_PARAMS::NO_DRILL_SHAPE ); if( plotOpt.GetFormat() == PLOT_FORMAT::DXF && plotOpt.GetDXFPlotPolygonMode() ) // PlotLayerOutlines() is designed only for DXF plotters. // and must not be used for other plot formats PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); else PlotSilkScreen( aBoard, aPlotter, layer_mask, plotOpt ); break; default: plotOpt.SetSkipPlotNPTH_Pads( false ); plotOpt.SetDrillMarksType( PCB_PLOT_PARAMS::NO_DRILL_SHAPE ); if( plotOpt.GetFormat() == PLOT_FORMAT::DXF && plotOpt.GetDXFPlotPolygonMode() ) // PlotLayerOutlines() is designed only for DXF plotters. // and must not be used for other plot formats PlotLayerOutlines( aBoard, aPlotter, layer_mask, plotOpt ); else PlotStandardLayer( aBoard, aPlotter, layer_mask, plotOpt ); break; } } } /* Plot a copper layer or mask. * Silk screen layers are not plotted here. */ void PlotStandardLayer( BOARD *aBoard, PLOTTER* aPlotter, LSET aLayerMask, const PCB_PLOT_PARAMS& aPlotOpt ) { BRDITEMS_PLOTTER itemplotter( aPlotter, aBoard, aPlotOpt ); itemplotter.SetLayerSet( aLayerMask ); EDA_DRAW_MODE_T plotMode = aPlotOpt.GetPlotMode(); // Plot edge layer and graphic items itemplotter.PlotBoardGraphicItems(); // Draw footprint texts: for( auto module : aBoard->Modules() ) { if( ! itemplotter.PlotAllTextsModule( module ) ) { wxLogMessage( _( "Your BOARD has a bad layer number for footprint %s" ), module->GetReference() ); } } // Draw footprint other graphic items: for( auto module : aBoard->Modules() ) { for( auto item : module->GraphicalItems() ) { if( item->Type() == PCB_MODULE_EDGE_T && aLayerMask[ item->GetLayer() ] ) itemplotter.Plot_1_EdgeModule( (EDGE_MODULE*) item ); } } // Plot footprint pads for( auto module : aBoard->Modules() ) { aPlotter->StartBlock( NULL ); for( auto pad : module->Pads() ) { if( (pad->GetLayerSet() & aLayerMask) == 0 ) continue; wxSize margin; double width_adj = 0; if( ( aLayerMask & LSET::AllCuMask() ).any() ) width_adj = itemplotter.getFineWidthAdj(); static const LSET speed( 4, B_Mask, F_Mask, B_Paste, F_Paste ); LSET anded = ( speed & aLayerMask ); if( anded == LSET( F_Mask ) || anded == LSET( B_Mask ) ) { margin.x = margin.y = pad->GetSolderMaskMargin(); } else if( anded == LSET( F_Paste ) || anded == LSET( B_Paste ) ) { margin = pad->GetSolderPasteMargin(); } // Now offset the pad size by margin + width_adj // this is easy for most shapes, but not for a trapezoid or a custom shape wxSize padPlotsSize; wxSize extraSize = margin * 2; extraSize.x += width_adj; extraSize.y += width_adj; // Store these parameters that can be modified to plot inflated/deflated pads shape wxSize deltaSize = pad->GetDelta(); // has meaning only for trapezoidal pads PAD_SHAPE_T padShape = pad->GetShape(); double padCornerRadius = pad->GetRoundRectCornerRadius(); if( pad->GetShape() == PAD_SHAPE_TRAPEZOID ) { // The easy way is to use BuildPadPolygon to calculate // size and delta of the trapezoidal pad after offseting: wxPoint coord[4]; pad->BuildPadPolygon( coord, extraSize/2, 0.0 ); // Calculate the size and delta from polygon corners coordinates: // coord[0] is the lower left // coord[1] is the upper left // coord[2] is the upper right // coord[3] is the lower right // the size is the distance between middle of segments // (left/right or top/bottom) // size X is the dist between left and right middle points: padPlotsSize.x = ( ( -coord[0].x + coord[3].x ) // the lower segment X length + ( -coord[1].x + coord[2].x ) ) // the upper segment X length / 2; // the Y size is the half sum // size Y is the dist between top and bottom middle points: padPlotsSize.y = ( ( coord[0].y - coord[1].y ) // the left segment Y lenght + ( coord[3].y - coord[2].y ) ) // the right segment Y lenght / 2; // the Y size is the half sum // calculate the delta ( difference of lenght between 2 opposite edges ) // The delta.x is the delta along the X axis, therefore the delta of Y lenghts wxSize delta; if( coord[0].y != coord[3].y ) delta.x = coord[0].y - coord[3].y; else delta.y = coord[1].x - coord[0].x; pad->SetDelta( delta ); } else padPlotsSize = pad->GetSize() + extraSize; // Don't draw a null size item : if( padPlotsSize.x <= 0 || padPlotsSize.y <= 0 ) continue; COLOR4D color = COLOR4D::BLACK; if( pad->GetLayerSet()[B_Cu] ) color = aPlotOpt.ColorSettings()->GetColor( LAYER_PAD_BK ); if( pad->GetLayerSet()[F_Cu] ) color = color.LegacyMix( aPlotOpt.ColorSettings()->GetColor( LAYER_PAD_FR ) ); // Temporary set the pad size to the required plot size: wxSize tmppadsize = pad->GetSize(); switch( pad->GetShape() ) { case PAD_SHAPE_CIRCLE: case PAD_SHAPE_OVAL: pad->SetSize( padPlotsSize ); if( aPlotOpt.GetSkipPlotNPTH_Pads() && ( aPlotOpt.GetDrillMarksType() == PCB_PLOT_PARAMS::NO_DRILL_SHAPE ) && ( pad->GetSize() == pad->GetDrillSize() ) && ( pad->GetAttribute() == PAD_ATTRIB_HOLE_NOT_PLATED ) ) break; itemplotter.PlotPad( pad, color, plotMode ); break; case PAD_SHAPE_RECT: if( margin.x > 0 ) { pad->SetShape( PAD_SHAPE_ROUNDRECT ); pad->SetSize( padPlotsSize ); pad->SetRoundRectCornerRadius( margin.x ); } // Fall through case PAD_SHAPE_TRAPEZOID: case PAD_SHAPE_ROUNDRECT: case PAD_SHAPE_CHAMFERED_RECT: pad->SetSize( padPlotsSize ); itemplotter.PlotPad( pad, color, plotMode ); break; case PAD_SHAPE_CUSTOM: { // inflate/deflate a custom shape is a bit complex. // so build a similar pad shape, and inflate/deflate the polygonal shape D_PAD dummy( *pad ); SHAPE_POLY_SET shape; pad->MergePrimitivesAsPolygon( &shape ); // Shape polygon can have holes so use InflateWithLinkedHoles(), not Inflate() // which can create bad shapes if margin.x is < 0 int maxError = aBoard->GetDesignSettings().m_MaxError; int numSegs = std::max( GetArcToSegmentCount( margin.x, maxError, 360.0 ), 6 ); shape.InflateWithLinkedHoles( margin.x, numSegs, SHAPE_POLY_SET::PM_FAST ); dummy.DeletePrimitivesList(); dummy.AddPrimitivePoly( shape, 0, false ); dummy.MergePrimitivesAsPolygon(); // Be sure the anchor pad is not bigger than the deflated shape because this // anchor will be added to the pad shape when plotting the pad. So now the // polygonal shape is built, we can clamp the anchor size if( margin.x < 0 ) // we expect margin.x = margin.y for custom pads dummy.SetSize( padPlotsSize ); itemplotter.PlotPad( &dummy, color, plotMode ); } break; } // Restore the pad parameters modified by the plot code pad->SetSize( tmppadsize ); pad->SetDelta( deltaSize ); pad->SetShape( padShape ); pad->SetRoundRectCornerRadius( padCornerRadius ); } aPlotter->EndBlock( NULL ); } // Plot vias on copper layers, and if aPlotOpt.GetPlotViaOnMaskLayer() is true, // plot them on solder mask GBR_METADATA gbr_metadata; bool isOnCopperLayer = ( aLayerMask & LSET::AllCuMask() ).any(); if( isOnCopperLayer ) { gbr_metadata.SetApertureAttrib( GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_VIAPAD ); gbr_metadata.SetNetAttribType( GBR_NETLIST_METADATA::GBR_NETINFO_NET ); } aPlotter->StartBlock( NULL ); for( auto track : aBoard->Tracks() ) { const VIA* Via = dyn_cast( track ); if( !Via ) continue; // vias are not plotted if not on selected layer, but if layer is SOLDERMASK_LAYER_BACK // or SOLDERMASK_LAYER_FRONT, vias are drawn only if they are on the corresponding // external copper layer LSET via_mask_layer = Via->GetLayerSet(); if( aPlotOpt.GetPlotViaOnMaskLayer() ) { if( via_mask_layer[B_Cu] ) via_mask_layer.set( B_Mask ); if( via_mask_layer[F_Cu] ) via_mask_layer.set( F_Mask ); } if( !( via_mask_layer & aLayerMask ).any() ) continue; int via_margin = 0; double width_adj = 0; // If the current layer is a solder mask, use the global mask clearance for vias if( aLayerMask[B_Mask] || aLayerMask[F_Mask] ) via_margin = aBoard->GetDesignSettings().m_SolderMaskMargin; if( ( aLayerMask & LSET::AllCuMask() ).any() ) width_adj = itemplotter.getFineWidthAdj(); int diameter = Via->GetWidth() + 2 * via_margin + width_adj; // Don't draw a null size item : if( diameter <= 0 ) continue; // Some vias can be not connected (no net). // Set the m_NotInNet for these vias to force a empty net name in gerber file gbr_metadata.m_NetlistMetadata.m_NotInNet = Via->GetNetname().IsEmpty(); gbr_metadata.SetNetName( Via->GetNetname() ); COLOR4D color = aPlotOpt.ColorSettings()->GetColor( LAYER_VIAS + static_cast( Via->GetViaType() ) ); // Set plot color (change WHITE to LIGHTGRAY because the white items are not seen on a // white paper or screen aPlotter->SetColor( color != WHITE ? color : LIGHTGRAY ); aPlotter->FlashPadCircle( Via->GetStart(), diameter, plotMode, &gbr_metadata ); } aPlotter->EndBlock( NULL ); aPlotter->StartBlock( NULL ); gbr_metadata.SetApertureAttrib( GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_CONDUCTOR ); // Plot tracks (not vias) : for( auto track : aBoard->Tracks() ) { if( track->Type() == PCB_VIA_T ) continue; if( !aLayerMask[track->GetLayer()] ) continue; // Some track segments can be not connected (no net). // Set the m_NotInNet for these segments to force a empty net name in gerber file gbr_metadata.m_NetlistMetadata.m_NotInNet = track->GetNetname().IsEmpty(); gbr_metadata.SetNetName( track->GetNetname() ); int width = track->GetWidth() + itemplotter.getFineWidthAdj(); aPlotter->SetColor( itemplotter.getColor( track->GetLayer() ) ); aPlotter->ThickSegment( track->GetStart(), track->GetEnd(), width, plotMode, &gbr_metadata ); } aPlotter->EndBlock( NULL ); // Plot filled ares aPlotter->StartBlock( NULL ); // Plot all zones of the same layer & net together so we don't end up with divots where // zones touch each other. std::set plotted; for( ZONE_CONTAINER* zone : aBoard->Zones() ) { if( !aLayerMask[ zone->GetLayer() ] || plotted.count( zone ) ) continue; plotted.insert( zone ); SHAPE_POLY_SET aggregateArea = zone->GetFilledPolysList(); bool needFracture = false; // If 2 or more filled areas are combined, resulting // aggregateArea will be simplified and fractured // (Long calculation time) for( ZONE_CONTAINER* candidate : aBoard->Zones() ) { if( !aLayerMask[ candidate->GetLayer() ] || plotted.count( candidate ) ) continue; if( candidate->GetNetCode() != zone->GetNetCode() ) continue; // Merging zones of the same net can be done only for areas // having compatible settings for drawings: // use or not outline thickness, and if using outline thickness, // having the same thickness // because after merging only one outline thickness is used if( candidate->GetFilledPolysUseThickness() != zone->GetFilledPolysUseThickness() ) // Should not happens, because usually the same option is used for filling continue; if( zone->GetFilledPolysUseThickness() && ( candidate->GetMinThickness() != zone->GetMinThickness() ) ) continue; plotted.insert( candidate ); aggregateArea.Append( candidate->GetFilledPolysList() ); needFracture = true; } if( needFracture ) { aggregateArea.Unfracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); aggregateArea.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); } itemplotter.PlotFilledAreas( zone, aggregateArea ); } aPlotter->EndBlock( NULL ); // Adding drill marks, if required and if the plotter is able to plot them: if( aPlotOpt.GetDrillMarksType() != PCB_PLOT_PARAMS::NO_DRILL_SHAPE ) itemplotter.PlotDrillMarks(); } // Seems like we want to plot from back to front? static const PCB_LAYER_ID plot_seq[] = { B_Adhes, // 32 F_Adhes, B_Paste, F_Paste, B_SilkS, B_Mask, F_Mask, Dwgs_User, Cmts_User, Eco1_User, Eco2_User, Edge_Cuts, Margin, F_CrtYd, // CrtYd & Body are footprint only B_CrtYd, F_Fab, B_Fab, B_Cu, In30_Cu, In29_Cu, In28_Cu, In27_Cu, In26_Cu, In25_Cu, In24_Cu, In23_Cu, In22_Cu, In21_Cu, In20_Cu, In19_Cu, In18_Cu, In17_Cu, In16_Cu, In15_Cu, In14_Cu, In13_Cu, In12_Cu, In11_Cu, In10_Cu, In9_Cu, In8_Cu, In7_Cu, In6_Cu, In5_Cu, In4_Cu, In3_Cu, In2_Cu, In1_Cu, F_Cu, F_SilkS, }; /* * Plot outlines of copper, for copper layer */ void PlotLayerOutlines( BOARD* aBoard, PLOTTER* aPlotter, LSET aLayerMask, const PCB_PLOT_PARAMS& aPlotOpt ) { BRDITEMS_PLOTTER itemplotter( aPlotter, aBoard, aPlotOpt ); itemplotter.SetLayerSet( aLayerMask ); SHAPE_POLY_SET outlines; for( LSEQ seq = aLayerMask.Seq( plot_seq, arrayDim( plot_seq ) ); seq; ++seq ) { PCB_LAYER_ID layer = *seq; outlines.RemoveAllContours(); aBoard->ConvertBrdLayerToPolygonalContours( layer, outlines ); outlines.Simplify( SHAPE_POLY_SET::PM_FAST ); // Plot outlines std::vector< wxPoint > cornerList; // Now we have one or more basic polygons: plot each polygon for( int ii = 0; ii < outlines.OutlineCount(); ii++ ) { for(int kk = 0; kk <= outlines.HoleCount (ii); kk++ ) { cornerList.clear(); const SHAPE_LINE_CHAIN& path = (kk == 0) ? outlines.COutline( ii ) : outlines.CHole( ii, kk - 1 ); for( int jj = 0; jj < path.PointCount(); jj++ ) cornerList.emplace_back( (wxPoint) path.CPoint( jj ) ); // Ensure the polygon is closed if( cornerList[0] != cornerList[cornerList.size() - 1] ) cornerList.push_back( cornerList[0] ); aPlotter->PlotPoly( cornerList, NO_FILL ); } } // Plot pad holes if( aPlotOpt.GetDrillMarksType() != PCB_PLOT_PARAMS::NO_DRILL_SHAPE ) { int smallDrill = (aPlotOpt.GetDrillMarksType() == PCB_PLOT_PARAMS::SMALL_DRILL_SHAPE) ? SMALL_DRILL : INT_MAX; for( auto module : aBoard->Modules() ) { for( auto pad : module->Pads() ) { wxSize hole = pad->GetDrillSize(); if( hole.x == 0 || hole.y == 0 ) continue; if( hole.x == hole.y ) { hole.x = std::min( smallDrill, hole.x ); aPlotter->Circle( pad->GetPosition(), hole.x, NO_FILL ); } else { // Note: small drill marks have no significance when applied to slots wxPoint drl_start, drl_end; int width; pad->GetOblongDrillGeometry( drl_start, drl_end, width ); aPlotter->ThickSegment( pad->GetPosition() + drl_start, pad->GetPosition() + drl_end, width, SKETCH, NULL ); } } } } // Plot vias holes for( auto track : aBoard->Tracks() ) { const VIA* via = dyn_cast( track ); if( via && via->IsOnLayer( layer ) ) // via holes can be not through holes { aPlotter->Circle( via->GetPosition(), via->GetDrillValue(), NO_FILL ); } } } } /* Plot a solder mask layer. * Solder mask layers have a minimum thickness value and cannot be drawn like standard layers, * unless the minimum thickness is 0. * Currently the algo is: * 1 - build all pad shapes as polygons with a size inflated by * mask clearance + (min width solder mask /2) * 2 - Merge shapes * 3 - deflate result by (min width solder mask /2) * 4 - ORing result by all pad shapes as polygons with a size inflated by * mask clearance only (because deflate sometimes creates shape artifacts) * 5 - draw result as polygons * * We have 2 algos: * the initial algo, that create polygons for every shape, inflate and deflate polygons * with Min Thickness/2, and merges the result. * Drawback: pads attributes are lost (annoying in Gerber) * the new algo: * create initial polygons for every shape (pad or polygon), * inflate and deflate polygons * with Min Thickness/2, and merges the result (like initial algo) * remove all initial polygons. * The remaining polygons are areas with thickness < min thickness * plot all initial shapes by flashing (or using regions) for pad and polygons * (shapes will be better) and remaining polygons to * remove areas with thickness < min thickness from final mask * * TODO: remove old code after more testing. */ #define NEW_ALGO 1 extern void KeepPolyInside( bool aInside ); void PlotSolderMaskLayer( BOARD *aBoard, PLOTTER* aPlotter, LSET aLayerMask, const PCB_PLOT_PARAMS& aPlotOpt, int aMinThickness ) { PCB_LAYER_ID layer = aLayerMask[B_Mask] ? B_Mask : F_Mask; // Set the current arc to segment max approx error int currMaxError = aBoard->GetDesignSettings().m_MaxError; aBoard->GetDesignSettings().m_MaxError = Millimeter2iu( 0.005 ); // We remove 1nm as we expand both sides of the shapes, so allowing for // a strictly greater than or equal comparison in the shape separation (boolean add) // means that we will end up with separate shapes that then are shrunk int inflate = aMinThickness/2 - 1; BRDITEMS_PLOTTER itemplotter( aPlotter, aBoard, aPlotOpt ); itemplotter.SetLayerSet( aLayerMask ); // Plot edge layer and graphic items. // They do not have a solder Mask margin, because they are graphic items // on this layer (like logos), not actually areas around pads. // Normal mode to generate polygons from shapes with arcs, if any: DisableArcRadiusCorrection( false ); itemplotter.PlotBoardGraphicItems(); for( auto module : aBoard->Modules() ) { for( auto item : module->GraphicalItems() ) { itemplotter.PlotAllTextsModule( module ); if( item->Type() == PCB_MODULE_EDGE_T && item->GetLayer() == layer ) itemplotter.Plot_1_EdgeModule( (EDGE_MODULE*) item ); } } // Build polygons for each pad shape. The size of the shape on solder mask should be size // of pad + clearance around the pad, where clearance = solder mask clearance + extra margin. // Extra margin is half the min width for solder mask, which is used to merge too-close shapes // (distance < aMinThickness), and will be removed when creating the actual shapes. // Will contain shapes inflated by inflate value that will be merged and deflated by // inflate value to build final polygons // After calculations the remaining polygons are polygons to plot SHAPE_POLY_SET areas; // Will contain exact shapes of all items on solder mask SHAPE_POLY_SET initialPolys; #if NEW_ALGO // Generate polygons with arcs inside the shape or exact shape // to minimize shape changes created by arc to segment size correction. DisableArcRadiusCorrection( true ); #endif // Plot pads for( auto module : aBoard->Modules() ) { // add shapes with their exact mask layer size in initialPolys module->TransformPadsShapesWithClearanceToPolygon( layer, initialPolys, 0 ); // add shapes inflated by aMinThickness/2 in areas module->TransformPadsShapesWithClearanceToPolygon( layer, areas, inflate ); } // Plot vias on solder masks, if aPlotOpt.GetPlotViaOnMaskLayer() is true, if( aPlotOpt.GetPlotViaOnMaskLayer() ) { // The current layer is a solder mask, use the global mask clearance for vias int via_clearance = aBoard->GetDesignSettings().m_SolderMaskMargin; int via_margin = via_clearance + inflate; for( auto track : aBoard->Tracks() ) { const VIA* via = dyn_cast( track ); if( !via ) continue; // vias are plotted only if they are on the corresponding external copper layer LSET via_set = via->GetLayerSet(); if( via_set[B_Cu] ) via_set.set( B_Mask ); if( via_set[F_Cu] ) via_set.set( F_Mask ); if( !( via_set & aLayerMask ).any() ) continue; // add shapes with their exact mask layer size in initialPolys via->TransformShapeWithClearanceToPolygon( initialPolys, via_clearance ); // add shapes inflated by aMinThickness/2 in areas via->TransformShapeWithClearanceToPolygon( areas, via_margin ); } } // Add filled zone areas. #if 0 // Set to 1 if a solder mask margin must be applied to zones on solder mask int zone_margin = aBoard->GetDesignSettings().m_SolderMaskMargin; #else int zone_margin = 0; #endif for( ZONE_CONTAINER* zone : aBoard->Zones() ) { if( zone->GetLayer() != layer ) continue; // Some intersecting zones, despite being on the same layer, cannot be // merged due to other parameters such as fillet radius. The filled areas will end up // effectively merged though, so we want to keep the corners of such intersections sharp. std::set colinearCorners; zone->GetColinearCorners( aBoard, colinearCorners ); // add shapes inflated by aMinThickness/2 in areas zone->TransformOutlinesShapeWithClearanceToPolygon( areas, inflate + zone_margin, false, &colinearCorners ); // add shapes with their exact mask layer size in initialPolys zone->TransformOutlinesShapeWithClearanceToPolygon( initialPolys, zone_margin, false, &colinearCorners ); } int maxError = aBoard->GetDesignSettings().m_MaxError; int numSegs = std::max( GetArcToSegmentCount( inflate, maxError, 360.0 ), 12 ); // Merge all polygons: After deflating, not merged (not overlapping) polygons // will have the initial shape (with perhaps small changes due to deflating transform) areas.Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); areas.Deflate( inflate, numSegs ); // Restore initial settings: aBoard->GetDesignSettings().m_MaxError = currMaxError; // Restore normal option to build polygons from item shapes: DisableArcRadiusCorrection( false ); #if !NEW_ALGO // To avoid a lot of code, use a ZONE_CONTAINER to handle and plot polygons, because our // polygons look exactly like filled areas in zones. // Note, also this code is not optimized: it creates a lot of copy/duplicate data. // However it is not complex, and fast enough for plot purposes (copy/convert data is only a // very small calculation time for these calculations). ZONE_CONTAINER zone( aBoard ); zone.SetMinThickness( 0 ); // trace polygons only zone.SetLayer( layer ); // Combine the current areas to initial areas. This is mandatory because inflate/deflate // transform is not perfect, and we want the initial areas perfectly kept areas.BooleanAdd( initialPolys, SHAPE_POLY_SET::PM_FAST ); areas.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); itemplotter.PlotFilledAreas( &zone, areas ); #else // Remove initial shapes: each shape will be added later, as flashed item or region // with a suitable attribute. // Do not merge pads is mandatory in Gerber files: They must be indentified as pads // we deflate areas in polygons, to avoid after subtracting initial shapes // having small artifacts due to approximations during polygon transforms areas.BooleanSubtract( initialPolys, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); // Slightly inflate polygons to avoid any gap between them and other shapes, // These gaps are created by arc to segments approximations areas.Inflate( Millimeter2iu( 0.002 ),6 ); // Now, only polygons with a too small thickness are stored in areas. areas.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); // Plot each initial shape (pads and polygons on mask layer), with suitable attributes: PlotStandardLayer( aBoard, aPlotter, aLayerMask, aPlotOpt ); // Add shapes corresponding to areas having too small thickness. std::vector cornerList; for( int ii = 0; ii < areas.OutlineCount(); ii++ ) { cornerList.clear(); const SHAPE_LINE_CHAIN& path = areas.COutline( ii ); // polygon area in mm^2 : double curr_area = path.Area() / ( IU_PER_MM * IU_PER_MM ); // Skip very small polygons: they are certainly artifacts created by // arc approximations and polygon transforms // (inflate/deflate transforms) constexpr double poly_min_area_mm2 = 0.01; // 0.01 mm^2 gives a good filtering if( curr_area < poly_min_area_mm2 ) continue; for( int jj = 0; jj < path.PointCount(); jj++ ) cornerList.emplace_back( (wxPoint) path.CPoint( jj ) ); // Ensure the polygon is closed if( cornerList[0] != cornerList[cornerList.size() - 1] ) cornerList.push_back( cornerList[0] ); aPlotter->PlotPoly( cornerList, FILLED_SHAPE ); } #endif } /** * Set up most plot options for plotting a board (especially the viewport) * Important thing: * page size is the 'drawing' page size, * paper size is the physical page size */ static void initializePlotter( PLOTTER *aPlotter, BOARD * aBoard, PCB_PLOT_PARAMS *aPlotOpts ) { PAGE_INFO pageA4( wxT( "A4" ) ); const PAGE_INFO& pageInfo = aBoard->GetPageSettings(); const PAGE_INFO* sheet_info; double paperscale; // Page-to-paper ratio wxSize paperSizeIU; wxSize pageSizeIU( pageInfo.GetSizeIU() ); bool autocenter = false; // Special options: to fit the sheet to an A4 sheet replace the paper size. However there // is a difference between the autoscale and the a4paper option: // - Autoscale fits the board to the paper size // - A4paper fits the original paper size to an A4 sheet // - Both of them fit the board to an A4 sheet if( aPlotOpts->GetA4Output() ) { sheet_info = &pageA4; paperSizeIU = pageA4.GetSizeIU(); paperscale = (double) paperSizeIU.x / pageSizeIU.x; autocenter = true; } else { sheet_info = &pageInfo; paperSizeIU = pageSizeIU; paperscale = 1; // Need autocentering only if scale is not 1:1 autocenter = (aPlotOpts->GetScale() != 1.0); } EDA_RECT bbox = aBoard->ComputeBoundingBox(); wxPoint boardCenter = bbox.Centre(); wxSize boardSize = bbox.GetSize(); double compound_scale; // Fit to 80% of the page if asked; it could be that the board is empty, in this case // regress to 1:1 scale if( aPlotOpts->GetAutoScale() && boardSize.x > 0 && boardSize.y > 0 ) { double xscale = (paperSizeIU.x * 0.8) / boardSize.x; double yscale = (paperSizeIU.y * 0.8) / boardSize.y; compound_scale = std::min( xscale, yscale ) * paperscale; } else compound_scale = aPlotOpts->GetScale() * paperscale; // For the plot offset we have to keep in mind the auxiliary origin too: if autoscaling is // off we check that plot option (i.e. autoscaling overrides auxiliary origin) wxPoint offset( 0, 0); if( autocenter ) { offset.x = KiROUND( boardCenter.x - ( paperSizeIU.x / 2.0 ) / compound_scale ); offset.y = KiROUND( boardCenter.y - ( paperSizeIU.y / 2.0 ) / compound_scale ); } else { if( aPlotOpts->GetUseAuxOrigin() ) offset = aBoard->GetAuxOrigin(); } aPlotter->SetPageSettings( *sheet_info ); aPlotter->SetViewport( offset, IU_PER_MILS/10, compound_scale, aPlotOpts->GetMirror() ); // Has meaning only for gerber plotter. Must be called only after SetViewport aPlotter->SetGerberCoordinatesFormat( aPlotOpts->GetGerberPrecision() ); aPlotter->SetDefaultLineWidth( aPlotOpts->GetLineWidth() ); aPlotter->SetCreator( wxT( "PCBNEW" ) ); aPlotter->SetColorMode( false ); // default is plot in Black and White. aPlotter->SetTextMode( aPlotOpts->GetTextMode() ); aPlotter->SetColorSettings( aPlotOpts->ColorSettings() ); } /** * Prefill in black an area a little bigger than the board to prepare for the negative plot */ static void FillNegativeKnockout( PLOTTER *aPlotter, const EDA_RECT &aBbbox ) { const int margin = 5 * IU_PER_MM; // Add a 5 mm margin around the board aPlotter->SetNegative( true ); aPlotter->SetColor( WHITE ); // Which will be plotted as black EDA_RECT area = aBbbox; area.Inflate( margin ); aPlotter->Rect( area.GetOrigin(), area.GetEnd(), FILLED_SHAPE ); aPlotter->SetColor( BLACK ); } /** * Calculate the effective size of HPGL pens and set them in the plotter object */ static void ConfigureHPGLPenSizes( HPGL_PLOTTER *aPlotter, PCB_PLOT_PARAMS *aPlotOpts ) { // Compute penDiam (the value is given in mils) in pcb units, with plot scale (if Scale is 2, // penDiam value is always m_HPGLPenDiam so apparent penDiam is actually penDiam / Scale int penDiam = KiROUND( aPlotOpts->GetHPGLPenDiameter() * IU_PER_MILS / aPlotOpts->GetScale() ); // Set HPGL-specific options and start aPlotter->SetPenSpeed( aPlotOpts->GetHPGLPenSpeed() ); aPlotter->SetPenNumber( aPlotOpts->GetHPGLPenNum() ); aPlotter->SetPenDiameter( penDiam ); } /** * Open a new plotfile using the options (and especially the format) specified in the options * and prepare the page for plotting. * Return the plotter object if OK, NULL if the file is not created (or has a problem) */ PLOTTER* StartPlotBoard( BOARD *aBoard, PCB_PLOT_PARAMS *aPlotOpts, int aLayer, const wxString& aFullFileName, const wxString& aSheetDesc ) { // Create the plotter driver and set the few plotter specific options PLOTTER* plotter = NULL; switch( aPlotOpts->GetFormat() ) { case PLOT_FORMAT::DXF: DXF_PLOTTER* DXF_plotter; DXF_plotter = new DXF_PLOTTER(); DXF_plotter->SetUnits( static_cast( aPlotOpts->GetDXFPlotUnits() ) ); plotter = DXF_plotter; break; case PLOT_FORMAT::POST: PS_PLOTTER* PS_plotter; PS_plotter = new PS_PLOTTER(); PS_plotter->SetScaleAdjust( aPlotOpts->GetFineScaleAdjustX(), aPlotOpts->GetFineScaleAdjustY() ); plotter = PS_plotter; break; case PLOT_FORMAT::PDF: plotter = new PDF_PLOTTER(); break; case PLOT_FORMAT::HPGL: HPGL_PLOTTER* HPGL_plotter; HPGL_plotter = new HPGL_PLOTTER(); // HPGL options are a little more convoluted to compute, so they get their own function ConfigureHPGLPenSizes( HPGL_plotter, aPlotOpts ); plotter = HPGL_plotter; break; case PLOT_FORMAT::GERBER: plotter = new GERBER_PLOTTER(); break; case PLOT_FORMAT::SVG: plotter = new SVG_PLOTTER(); break; default: wxASSERT( false ); return NULL; } // Compute the viewport and set the other options // page layout is not mirrored, so temporarily change mirror option for the page layout PCB_PLOT_PARAMS plotOpts = *aPlotOpts; if( plotOpts.GetPlotFrameRef() && plotOpts.GetMirror() ) plotOpts.SetMirror( false ); initializePlotter( plotter, aBoard, &plotOpts ); if( plotter->OpenFile( aFullFileName ) ) { plotter->ClearHeaderLinesList(); // For the Gerber "file function" attribute, set the layer number if( plotter->GetPlotterType() == PLOT_FORMAT::GERBER ) { bool useX2mode = plotOpts.GetUseGerberX2format(); GERBER_PLOTTER* gbrplotter = static_cast ( plotter ); gbrplotter->UseX2format( useX2mode ); gbrplotter->UseX2NetAttributes( plotOpts.GetIncludeGerberNetlistInfo() ); // Attributes can be added using X2 format or as comment (X1 format) AddGerberX2Attribute( plotter, aBoard, aLayer, not useX2mode ); } plotter->StartPlot(); // Plot the frame reference if requested if( aPlotOpts->GetPlotFrameRef() ) { PlotWorkSheet( plotter, aBoard->GetProject(), aBoard->GetTitleBlock(), aBoard->GetPageSettings(), 1, 1, aSheetDesc, aBoard->GetFileName() ); if( aPlotOpts->GetMirror() ) initializePlotter( plotter, aBoard, aPlotOpts ); } // When plotting a negative board: draw a black rectangle (background for plot board // in white) and switch the current color to WHITE; note the color inversion is actually // done in the driver (if supported) if( aPlotOpts->GetNegative() ) { EDA_RECT bbox = aBoard->ComputeBoundingBox(); FillNegativeKnockout( plotter, bbox ); } return plotter; } delete plotter; return NULL; }