kicad/pcbnew/plot_board_layers.cpp

1270 lines
46 KiB
C++

/**
* @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 <fctsys.h>
#include <common.h>
#include <plotter.h>
#include <base_struct.h>
#include <gr_text.h>
#include <geometry/geometry_utils.h>
#include <trigo.h>
#include <pcb_base_frame.h>
#include <macros.h>
#include <math/util.h> // for KiROUND
#include <class_board.h>
#include <class_module.h>
#include <class_track.h>
#include <class_edge_mod.h>
#include <class_pcb_text.h>
#include <class_zone.h>
#include <class_drawsegment.h>
#include <class_pcb_target.h>
#include <class_dimension.h>
#include <pcbnew.h>
#include <pcbplot.h>
#include <pcb_painter.h>
#include <gbr_metadata.h>
/*
* 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->RenderSettings()->GetLayerColor( B_SilkS );
else if( layersmask_plotpads[F_SilkS] )
color = aPlotter->RenderSettings()->GetLayerColor( F_SilkS );
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<const VIA*>( 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<int>( 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() ) );
if( track->Type() == PCB_ARC_T )
{
ARC* arc = static_cast<ARC*>( track );
VECTOR2D center( arc->GetCenter() );
auto radius = arc->GetRadius();
auto start_angle = arc->GetArcAngleStart();
auto end_angle = start_angle + arc->GetAngle();
aPlotter->ThickArc( wxPoint( center.x, center.y ), -end_angle, -start_angle,
radius, width, plotMode, &gbr_metadata );
}
else
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<ZONE_CONTAINER*> 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<const VIA*>( 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<const VIA*>( 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<VECTOR2I> 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<wxPoint> 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->SetCreator( wxT( "PCBNEW" ) );
aPlotter->SetColorMode( false ); // default is plot in Black and White.
aPlotter->SetTextMode( aPlotOpts->GetTextMode() );
}
/**
* 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<DXF_PLOTTER::DXF_UNITS>( 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;
}
KIGFX::PCB_RENDER_SETTINGS* renderSettings = new KIGFX::PCB_RENDER_SETTINGS();
renderSettings->LoadColors( aPlotOpts->ColorSettings() );
renderSettings->SetDefaultPenWidth( aPlotOpts->GetLineWidth() );
plotter->SetRenderSettings( renderSettings );
// 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 <GERBER_PLOTTER*> ( 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->RenderSettings();
delete plotter;
return NULL;
}