classdef PCBWriter < handle & matlab.mixin.CustomDisplay &...
                    matlab.mixin.SetGet
    %PCBWriter Create PC board definitions from 2D antenna designs.
    %   B = PCBWriter(D) creates an PCBWriter object that can generate
    %   Gerber-format PC board (PCB) design files based on a multi-layer,
    %   2D antenna design described by a pcbStack object D,from Antenna
    %   Toolbox.
    %
    %   gerberWrite(B) generates Gerber-format PCB files from PCBWriter
    %   object B, either as a set of individual files or as a single
    %   archive file.  Files are generated based on design D and the value
    %   of several properties described below.
    %
    %   PCBWriter object B contains properties that can customize the
    %   files generated for use with various PCB services, such as online
    %   PCB manufacturing and visualization. Many of these properties are
    %   grouped into two objects stored in two properties of B:
    %      B.Writer    - properties related to Gerber file generation
    %      B.Connector - properties related to optional RF connectors
    %
    %   For example, the following code specifies that a Cinch SMA
    %   connector is to be added to the antenna feedpoint, and the Mayhew
    %   Labs PCB viewer will be used to view the PCB design specified by p,
    %   a pcbStack antenna:
    %      B = PCBWriter(p);
    %      B.Writer = PCBServices.MayhewWriter;
    %      B.Connector = PCBConnectors.SMA_Cinch;
    %      gerberWrite(B)
    %
    %   See property help for more details on Writer and Connector objects.
    %   Values for antenna D, writer W and connector C objects may be passed
    %   to the constructor for convenience using the following syntax:
    %      B = PCBWriter(D,W,C);
    %      B = PCBWriter(D,W);
    %      B = PCBWriter(D,C);
    %      B = PCBWriter(D);
    %
    %   W may be substituted by the name of the output folder or file, in
    %   which case a default Gerber.Writer is created.
    %
    %   See also gerberWrite, Writer, Connector, PCBServices,
    %   PCBConnectors, info.

%   Copyright 2017-2020 The MathWorks, Inc.
    
    % TODO
    % - Ohm symbol switch when using MEX file
    % - Stroke fonts vs polygon fonts
    
    properties (SetAccess = private)
        %Design Data structure describing 2D antenna design geometry.
        %  Design is a read-only property containing a data structure that
        %  describes the geometry of a 2D antenna design.  The data
        %  structure is typically created by exporting a design from
        %  Antenna Toolbox.
        %
        %  Load a new design by calling the load method.
        %
        %  Some 2D antenna designs may be manufactured as a multi-layer
        %  printed circuit board.  The PCB must be designed as a set of
        %  planar layers of metal regions, each layer separated by
        %  dielectric material.  A single type of metal and dielectric
        %  substrate is assumed; typically these are copper and FR4, but
        %  other materials may be specified.
        %
        %  To specify the geometry and electrical properties of a 2D
        %  antenna design, the following fields must be present in the
        %  antenna geometry structure, where all sizes are in meters.
        %  Equivalently, these fields must be present as distinct variables
        %  in a MAT file.
        %
        %  Layers: Vector of structures describing all metal layers,
        %     with each layer of metal described as one structure. The
        %     index of each structure in the Layers vector defines the
        %     stacking order of the layer in the PCB, where index 1 is the
        %     top layer and the last index is the bottom layer.
        %
        %     Each structure must have the fields:
        %
        %     .Fills: Cell vector specifying filled metal polygons in each
        %     PCB layer, {F1, F2, ... FN}, where each entry Fi describes
        %     all polygons in the i'th PCB layer.
        %
        %     Typically, each Fi entry is a cell vector, e.g., Fi = {Pi1,
        %     Pi2, ...}, where Pij is the j'th 2D polygon for layer i.  Pij
        %     is an Nx2 matrix with adjacent [x y] vertices in each row.
        %     The number of rows can vary for each polygon.
        %
        %     If only a single polygon is required for layer i, Fi can be
        %     the matrix Pi1 with no parent cell.
        %
        %     .Holes: Cell vector of 2D polygon matrices.  Specifies zero
        %     or more metal-free areas to clear within each metal polygon
        %     for a layer.  Can be an empty cell vector if no holes are
        %     specified in any layer, or it must have the same length as
        %     the .Fills cell vector.
        %
        %     All entries in .Holes have the same specification and options
        %     as defined for the .Fills field.  All holes must be contained
        %     completely within a .Fills polygon, and multiple holes can be
        %     specified within a single fill.
        %
        %  Substrate: Overall thickness of PCB layer in meters.
        %
        %  ViaLocations: Cell vector specifying electrical
        %     connections (shorts) between pairs of metal layers in the
        %     antenna at specified locations.  Each cell entry is [x y
        %     fromLayer toLayer].  The top layer is 1 and the bottom layer
        %     is N for a design with N layers of metal. If no shorts are
        %     needed, this may be an empty matrix. If a single short is
        %     needed, a vector may be specified instead of a cell vector.
        %
        %  FeedLocations: Cell vector defining antenna feedpoints.
        %     Each cell entry can take the general form:
        %           [xSig ySig layerSig xGnd yGnd layerGnd]
        %     where Sig is the signal and Gnd is the ground.
        %
        %     When a single feedpoint is specified, a vector may be
        %     specified instead of a cell vector.
        %
        %     Antenna feed-line connection points may be specified on the
        %     top or bottom layer of the PCB only. If the signal is on the
        %     top (bottom) layer, the ground is on the bottom (top) layer,
        %     and the signal and ground points correspond, the vector can
        %     be specified as just [x y layerSig].
        %
        %  DefaultConnector: Default RF connector to add to the PCB.
        %     This is the default RF connector to add when no explicit
        %     Connector is specified.
        %
        %     Value may be empty, implying no RF connector is added to the
        %     design by default.  Otherwise, the value may be a
        %     BaseConnector object, or a string or function handle that
        %     creates a BaseConnector object using feval().
        %
        %     When not empty, this should be a specific connector that can
        %     be implemented, not a generic style, so that the PCB is fully
        %     specified and can be manufactured. Example: DefaultConnector
        %     = 'PCBConnectors.SMAEdge_Samtec'
        %
        %  DesignInfo: Structure of text that is added to PCB.
        %      This is informative text that is visible on a PCB design
        %      when visualized in software, but is placed outside the
        %      physical board profile such that the manufactured board will
        %      not contain the text. All structure fields are optional.
        %       .Title: String with name of antenna.
        %               If omitted, line of text is suppressed.
        %       .Comment: String specifying frequency, gain, etc
        %               If omitted, default text is added.
        %       .Attribute: Text with design revision, date, etc
        %               If omitted, date of file creation is added.
        %
        % See also load, PCBWriter.
        Design
    end
    
    properties %Access = ?PCBConnectors.BaseConnector)
        %Writer Customize PCB file generation for a given PCB service.
        %   Set the value of Writer to an object of a subclass of
        %   Gerber.Writer to specify how PCB files are generated.  Many
        %   Writer subclasses may be found in the PCBServices package.
        %
        %   All Writer properties are configured at once by specifying a
        %   PCB file writer object as the value of the Writer property. For
        %   example,
        %      B.Writer = PCBServices.MayhewWriter;
        %   creates a MayhewWriter object and specifies it for PCB file
        %   generation.  All Writer properties are then customized to work
        %   with the Mayhew Labs online 3D PCB visualization service.
        %
        %   If a Writer object is not explicitly specified, a default
        %   writer is used to generate generic Gerber files.  Writer
        %   properties may be modified at any time prior to calling the
        %   generate(B) method. For example,
        %      B.Writer.CreateArchiveFile = true;
        %   specifies that a single ZIP file is to be generated instead of
        %   a set of individual Gerber files.
        %
        %   New Writer classes may be created by copying an existing Writer
        %   class and modifying it to meet the requirements of a new PCB
        %   service.
        %
        %   See also PCBWriter, Design, load, PCBServices, Connector,
        %   PCBConnectors, generate.
        Writer
    end
    
    properties (Dependent,AbortSet) % don't update unless it's a change
        %Connector Specify an RF connector at the antenna feedpoint.
        %   Set the value of Connector to an object of a subclass of
        %   PCBConnectors.BaseConnector to specify an RF connector that is
        %   added to the antenna feedpoint when PCB files are generated.
        %
        %   The result is generally a set of modifications to the PCB
        %   design files that includes new copper landing pads and traces,
        %   and changes to solder mask, silk screen and solder paste files.
        %
        %   All Connector properties are configured at once by specifying a
        %   PCB connector object as the value of the Connector property.
        %   All Connector properties are then configured automatically.
        %   For example,
        %      B.Connector = PCBConnectors.SMA_Cinch;
        %   creates an SMA_Cinch RF connector object and specifies that it
        %   should be added to the PCB at the antenna feedpoint when PCB
        %   files are generated.
        %
        %   An empty matrix indicates that a Connector object is not
        %   explicitly specified, and a default Connector is chosen based
        %   on the antenna design.  If an RF connector is not desired, use
        %   an empty matrix for B.Connector, and set
        %       B.UseDefaultConnector = false;
        %   This ensures no RF connector gets added to the PCB files.
        %
        %   Connector properties may be modified at any time prior to
        %   calling the generate(B) method.  For example, to increase the
        %   diameter of copper around the center pin from 1mm to 1.5mm,
        %   change the PinDiameter property of the connector as follows:
        %      B.Connector = SMA_Cinch;
        %      B.Connector.PinDiameter = 1.5e-3; % specified in meters
        %
        %   New connectors may be created by copying an existing connector
        %   class and modifying it to generate the layout for a new PCB
        %   connector.
        %
        %   See also PCBWriter, Design, load, PCBConnectors, Writer,
        %   PCBServices, generate.
        Connector
    end
    
    properties
        %UseDefaultConnector Set to true to use the default RF connector.
        %  When true, the connector specified in the property
        %  Design.DefaultConnector is utilized.  If this field is not
        %  present in the Design data or is empty, no connector is added to
        %  the PCB by default.
        %
        %  When false, the connector specified by Connector is utilized.
        %
        %  If the Connector property is set to a new value, the value of
        %  UseDefaultConnector is set to false automatically.
        UseDefaultConnector = true
    end
    
    properties
        % Width of lines drawn around components on silkscreen, in mils
        % (1 mil = 1/1000")
        ComponentBoundaryLineWidth = 8  % mils
        
        % Font size used to label components on silkscreen, in points. Set
        % to empty for automatic font sizing.
        ComponentNameFontSize = []
        
        % Font size used for design information text added outside the
        % board profile, in points.  Set to empty for automatic font
        % sizing.
        DesignInfoFontSize = []
        
        % Font Name of font to use for component name and design info.
        Font = 'Arial'                   % simple and ubiquitous
        %Font = 'Calibri'                % simple
        %Font = 'Comic Sans MS';         % hand-written simple
        %Font = 'Arial Rounded MT Bold'; % high visibility
        %Font = 'Bradley Hand ITC';      % hand-written
        %Font = 'Script MT Bold';        % fancy
                
        %PCBMargin Copper-free margin around board to maintain, in meters.
        %   Often specified by PCB manufacturers as "minimum circuit to
        %   edge distance."
        PCBMargin = 0.5e-3
        
        % Adds soldermask to top and/or bottom of PCB.
        % Choices are 'none', neither', 'top', 'bottom', 'both'
        Soldermask = 'both'

        % Generate solder paste files as part of PCB stack.
        Solderpaste = true
    end
    
    properties (Hidden)
        % Define height of text for component annotations and design info.
        % Text height is specified by the equivalent number of lines of
        % text that would fill the PCB height.
        DesiredLinesOfFontInfo = 20
        
        % Minimum Info font size, in mils (1/1000")
        MinInfoFontSize = 1/72*1000 % 1 pt, in mils
        
        % Maximum Info font size, in mils (1/1000")
        MaxInfoFontSize = 14/72*1000 % 14 pt, in mils
        
        DesignInfoSeparationLineWidth = 4 % mils

    end
    
    properties (Access = private)
        % Retains a copy of the BaseConnector object, updated at the start
        % of the generate method
        CachedConnector
    end
    
    properties (Access = ?PCBConnectors.BaseConnector)
        % Smallest bounding box enclosing antenna "electrical" regions,
        % including copper, vias, etc.
        AntennaBoundingBox
        
        % Scaled version of Data (which is always in meters), scaled to
        % units of Gerber.Writer (whatever they are):
        ScaledDesign
    end
    properties(Access=private)
        privateConnector   = []
    end
    
    properties (Access=private,Constant)
        ReqdTopFields    = {'Layers','FeedLocations'}
        ReqdLayersFields = {'Fills', 'Holes','Substrate'}
        OptTopFields     = {'ViaLocations','DesignInfo'}
    end
    
    % Constructor and set/get
    %
    methods
        function B = PCBWriter(D,arg2,varargin)
            %Constructor - see detailed help at top of class file.
            %  B = PCBWriter
            %  B = PCBWriter(Design)
            %  B = PCBWriter(Design,Writer)
            %  B = PCBWriter(Design,Connector)
            %  B = PCBWriter(Design,Writer,Connector)
            %
            %  Design: DNAME or struct
            %  Writer: WNAME or subclass of Gerber.Writer
            %  Connector: Subclass of PCBConnectors.BaseConnector
            %
            %  If DNAME specified but neither WNAME nor Writer, the base of
            %  DNAME is used as WNAME when constructing new Writer object.
            
            W = [];
            C = [];
            Dname = '';
            if nargin > 0
                if ischar(D)
                    % PCBWriter(DNAME, ...)
                    Dname = D;
                    load(B,Dname);
                elseif isobject(D)
                    if isa(D,'pcbStack')||isa(D,'pcbComponent')
                        createGeometry(D);
                        G = exportGeometry(D);
                        if isa(D,'pcbComponent')&& numel(G.Layers)>2
                            error(message('rfpcb:rfpcberrors:Unsupported','More than 2 layers of metal','generating gerber files'))
                        end
                        if isa(D,'pcbStack')&& numel(G.Layers)>2
                            error(message('antenna:antennaerrors:Unsupported','More than 2 layers of metal','generating gerber files'))
                        end  
                            
                        B.Design = G;
                        numfeeds = size(D.FeedLocations,1);
                    else
                        error(message('antenna:antennaerrors:InputNotPCBStack'));
                        %('Expected input to be a pcbStack antenna');
                    end
                else
                    % PCBWriter(D, ...)
                    B.Design = D;
                    numfeeds = numel(D.FeedLocations);
                    
                end
            end
            
            if nargin > 2
                % PCBWriter(D,W,C)
                W = arg2;
                if isequal(numel(varargin),1)
                    C = varargin{:};
                else
                    C = varargin;
                end
                
            elseif nargin > 1
                % PCBWriter(D,W)
                % PCBWriter(D,C)
                if isa(arg2,'PCBConnectors.BaseConnector')
                    C = arg2;
                else
                    W = arg2;
                end
            end
            
            if ~iscell(C) && numel(C)>1
                error(message('antenna:antennaerrors:ExpectedCellArray'));
            end
            
            % Check if scalar connector and more than 1-feed - scalar
            % expansion
            if isscalar(C) && numfeeds>1
                Ctemp = C;
                C = cell(1,numfeeds);
                [C{:}] = deal(Ctemp);
            end
            
            % Check if cell array that the number of elements is equal to number
            % of feed points
            if ~isempty(C)
                if ~isequal(numel(C),numfeeds)
                    error(message('antenna:antennaerrors:ConnectorArraySizeMismatch'));
                end
            end
            
            if ~isempty(W)
                if ischar(W)
                    % PCBWriter(D,WNAME)
                    B.Writer = Gerber.Writer(W);
                else
                    % PCBWriter(D,W)
                    B.Writer = W;
                end
            else
                % Create general-purpose Gerber.Writer instance.
                %
                % If a source file was passed for the Design, use the same
                % file folder/name for output Writer file, stripping away
                % any extension -- so either a set of files or a ZIP file
                % will be created with this name.
                %
                if ~isempty(Dname)
                    [p,n] = fileparts(Dname);
                    Wname = fullfile(p,n); % no extention
                    B.Writer = Gerber.Writer(Wname);
                else
                    % Use default Wname ('untitled')
                    B.Writer = Gerber.Writer;
                end
            end
            
            if iscell(C)
                connectorExists = any(cellfun(@(x) ~isempty(x),C));
            else
                connectorExists = ~isempty(C);
            end
                         
            if connectorExists
                B.Connector = C;
            end
            
        end
        
        function set.Design(B,design)
            % Check and set antenna geometry design structure.
            checkDataStruct(B,design);
            B.Design = design;
        end
        
        function set.Writer(B,val)
            if ~isa(val,'Gerber.Writer')
                error(message('antenna:antennaerrors:InvalidWriterSpecified'));
            end
            B.Writer = val;
        end
        
        function set.Connector(B,val)
            invalidConnector = false;
            if iscell(val)
                nonEmptyConnector = cellfun(@(x) ~isempty(x),val);
                nonemptyConnectorCheck = any(nonEmptyConnector);
                valCheck = val(nonEmptyConnector);
                if  nonemptyConnectorCheck ...
                        && any(cellfun(@(x) ~ischar(x),valCheck)) ...
                        && any(cellfun(@(x) ~isa(x,'function_handle'),valCheck)) ...
                        && any(cellfun(@(x) ~isa(x,'PCBConnectors.BaseConnector'),valCheck))
                    invalidConnector = true;
                end
                
            else
                nonemptyConnectorCheck = ~isempty(val);
                if  nonemptyConnectorCheck ...
                        && ~ischar(val) ...
                        && ~isa(val,'function_handle') ...
                        && ~isa(val,'PCBConnectors.BaseConnector')
                    invalidConnector = true;
                end
            end
            if invalidConnector
                error(message('antenna:antennaerrors:InvalidPCBConnectorSpecified'));
            end
            if isscalar(val) && (numel(B.Design.FeedLocations)>1)
                Ctemp = val;
                val = cell(1,numel(B.Design.FeedLocations));
                [val{:}] = deal(Ctemp);
            end
            
            B.privateConnector = val;
            
            % Side effect:
            % Use the Connector if a non-empty object has been
            % set.
            %
            % If an empty object is set, don't change.  That implies we
            % either continue to leave Use=false (and use the empty, which
            % means, use no connector).  Or it means Use=true and we're
            % setting Feedpoint=[], which might mean to start using nothing
            % --- but we continue to use the Default object nevertheless.
            % Empty doesn't change the flag.
            if nonemptyConnectorCheck
                B.UseDefaultConnector = false; %#ok<MCSUP>
            end
        end

        function  propVal= get.Connector(B)
            propVal=B.privateConnector;
        end
        
        function set.UseDefaultConnector(obj,tf)
            % Must be 0, 1, true or false
            validateattributes(tf,{'logical','numeric'}, ...
                {'scalar','real','finite'}, ...
                '','UseDefaultConnector');
            obj.UseDefaultConnector = logical(tf);
        end
        
        function set.ComponentBoundaryLineWidth(obj,val)
            % Must be positive value
            validateattributes(val,{'numeric'}, ...
                {'nonnan','real','finite','nonnegative'}, ...
                '','ComponentBoundaryLineWidth');
            obj.ComponentBoundaryLineWidth = (val);
        end
        
        function set.PCBMargin(obj,val)
            % Must be positive value
            validateattributes(val,{'numeric'}, ...
                {'nonnan','real','finite','nonnegative'}, ...
                '','PCBMargin');
            obj.PCBMargin = (val);
        end
        
        function set.ComponentNameFontSize(obj,val)
            % Must be positive value
            validateattributes(val,{'numeric'}, ...
                {'nonnan','finite','integer','nonnegative','nonzero'}, ...
                '','ComponentNameFontSize');
            obj.ComponentNameFontSize = (val);
        end
        
        function set.DesignInfoFontSize(obj,val)
            % Must be positive value
            validateattributes(val,{'numeric'}, ...
                {'nonnan','finite','integer','nonnegative','nonzero'}, ...
                '','DesignInfoFontSize');
            obj.DesignInfoFontSize = (val);
        end
        
        function set.Soldermask(obj,val)
            % Adds soldermask to top and/or bottom of PCB.
            % Choices are 'none', 'neither', 'top', 'bottom', 'both'
            obj.Soldermask = validatestring(val,{'none','neither','top','bottom','both'},'','Soldermask');
        end
        
        function set.Solderpaste(obj,tf)
            % Must be 0, 1, true or false
            validateattributes(tf,{'logical','numeric'}, ...
                {'scalar','real','finite'}, ...
                '','Solderpaste');
            obj.Solderpaste = logical(tf);
        end
    end
    
    % Main public methods
    %
    methods
        function gerberWrite(B)
            %Generate Generate PCB specification files.
            %   generate(B) generates Gerber-format PCB files from
            %   PCBWriter object B, either as a set of individual
            %   files or as a single archive file.  Files are generated
            %   based on properties of object B, and properties within each
            %   of the Design, Writer and Connector objects.
            %
            %   See also PCBWriter, Design, load, Writer, PCBServices,
            %   Connector, PCBConnectors.
            
            update(B); % compile design
            
            % This step is unique to generate(), not part of compile()
            % - needed for final steps of PCB generation (text/fonts)
            % - it doesn't add any "compile" meta-data
            % - it's very time consuming and shouldn't be in update()
            addDesignInfo(B);
            
            % Write the files, invoke post-write fcn
            write(B.Writer);
            
            % NOTE: Can call write(W,true) to open folder when written,
            % or openFolder(W), info(W) etc.
        end
        
        function update(B)
            % Compile PCB design and update summary information.
            
            % Reinitialize Gerber.Writer to reset file set, aperture
            % manager, etc.  This matters when generate() is called for a
            % second or subsequent time.
            init(B.Writer);
            resetCache(B);
            
            if ~isMetalSpecified(B)
                warning(message('antenna:antennaerrors:NoMetalOnPCB'));
                %('No metal specified for PCB.');
            end
            
            % Generate all required PCB layers
            
            % Since we write a profile even with "no metal", we must set
            % coord precision.
            %
            % Only set coords AFTER .Writer property has been set
            rescaleDesign(B);  % rescale before setting coord precision
            setCoordPrecision(B);
            
            writeAntennaMetal(B);
            addExplicitVias(B);
            addSoldermask(B);
            addConnectors(B);
            addProfile(B);
            addSummaryInfoFiles(B);
            addNetlistFiles(B); % must appear AFTER addConnectors
        end
        
        function info(B)
            %INFO Display PCB file stack summary in command window.
            %   INFO(B) shows a summary of Gerber files in the command
            %   window.
            %
            %   If no Writer object was specified, no summary is shown.
            
            if ~isempty(B.Writer)
                var_name = [inputname(1) '.Writer'];
                info(B.Writer,var_name);
                if ~isempty(B.Connector)
                    show(B.Connector);
                end
                fprintf(getBoardSummaryString(B));
            end
        end
        
        function openFolder(B)
            %OPEN Open a file browser to the generated file folder.
            %  OPEN(B) opens a file browser to the parent folder of the
            %  Gerber file.
            %
            %   If no Writer object was specified, no browser is opened.
            
            if ~isempty(B.Writer)
                openFolder(B.Writer);
            end
        end
        
        function sendTo(B)
            %sendTo Export generated PCB files to service provider.
            %   sendTo(B) sends the generated PCB design files to the
            %   service provider defined by B.Service, where B is an
            %   PCBWriter object.
            %
            %   For example, if B.Service = PCBServices.MayhewWriter, the
            %   sendTo action opens the MayhewLabs online PCB viewer in a
            %   web browser and opens the folder containing the Gerber
            %   files.  This simplifies use of the service, enabling you to
            %   easily drag and drop the files to the website and view the
            %   design.
            %
            %   If B.Service is empty, the generic GerberWriter service is
            %   utilized and no further action is taken by sendTo.
            
            if ~isempty(B.Writer)
                sendTo(B.Writer);
            end
        end
    end

    
    methods (Access=protected)
        function footer = getFooter(B)
            % Add footer to command-window object display
            
            %footer = getBoardSummaryString(B);
            
            var = inputname(1);
            if isempty(var)
                footer = '';
            else
                footer = sprintf('   See <a href="matlab:info(%s)">info</a> for details',var);
            end
        end
        
        function str = getBoardSummaryString(B)
            % Add footer to command-window object display
            % - inch and mm dimensions
            % - sq in and sq mm area
            
            W = B.Writer;
            Nlayer = getNumCopperLayers(W);
            
            if Nlayer==0
                % No board data
                % - either no design, or not yet compiled
                if isempty(B.Design)
                    msg = getString(message('antenna:emcatalog:LoadDesign'));
                else
                    msg = getString(message('antenna:emcatalog:UpdateorGeneratePCB'));
                end
                str = getString(message('antenna:emcatalog:Boardsummary', msg));
            else
                % Summary of board dimensions
                p_in = getBoardProfile(B,'in');
                p_mm = getBoardProfile(B,'mm');
                
                sq_in = p_in(3)*p_in(4);
                sq_mm = p_mm(3)*p_mm(4);
                sqChar = native2unicode([194 178],'utf-8');
                
                str = sprintf(getString(message('antenna:emcatalog:Boardsummarylayers','%d','\n','%.2f x %.2f','(%.2f in%c)\n','%.2f x %.2f','(%.2f mm%c)\n')), ...
                    Nlayer, ...
                    p_in(3),p_in(4), sq_in,sqChar, ...
                    p_mm(3),p_mm(4), sq_mm,sqChar);
            end
        end
    end
    
    % Summary file generation methods
    %
    methods (Access=protected)
        function addSummaryInfoFiles(B)
            % Add summary files to the PCB file stack. These don't affect
            % PCB generation directly; they are typically read by people to
            % assess overall manufacturing steps and requirements.
            %
            % Two summary files are generated:
            %  - Photoplotter Info File
            %  - Drill Station Info File
            %
            % These are both generated using the EagleCAD info file format
            % generators.
            
            W = B.Writer;
            
            % xxx Consider suppressing info files if no content?
            %
            F = Gerber.PhotoplotterInfo.getOrCreate(W);
            generateEagle(F);
            F = Gerber.DrillStationInfo.getOrCreate(W);
            generateEagle(F);
        end
    end
    
    % Netlist generation methods
    %
    methods (Access=protected)
        function addNetlistFiles(B)
            % Add netlist files to the PCB file stack. These don't affect
            % PCB generation directly; they are typically used to add board
            % component visualizations to the rendered board.
            %
            % Only call this after addConnectors() is called, so that
            % fc.PCBWriter is set first.
            
            W = B.Writer;
            F = Gerber.IPCD356A.getOrCreate(W);
            % NOTE: If no feedpoint connector, fc will be empty
            fc = getConnector(B);
            generate(F,fc);
        end
    end
    
    % Feedpoint methods
    %
    methods (Access=protected)
        function resetCache(B)
            % Reset all cached data
            B.CachedConnector = [];
            B.AntennaBoundingBox = [];
            B.ScaledDesign = [];
        end
        
        function fc = getConnector(B)
            % Get current PCBConnectors.BaseConnector object, which may be
            % empty.
            %
            % Returns B.Connector only if B.UseDefaultConnector is false.
            %
            % Otherwise returns B.Design.DefaultConnector.
            %
            % If the value is not a BaseConnector object, an attempt is
            % made to convert it to an object using feval.
            
            fc = B.CachedConnector;
            if ~isempty(fc)
                if ~iscell(fc)
                    fc = {fc};
                end
              return
            end
        
            if B.UseDefaultConnector
                % Use a default feedpoint connector object, if one is
                % present in MAT file data structure in the field:
                %   .DefaultConnector
                %
                % Otherwise, we don't use any connector.
                
                d = B.Design;
                if isfield(d,'DefaultConnector')
                    % Get feedpoint connector from MAT file.
                    %
                    % Empty matrix is allowed/supported.
                    fc = d.DefaultConnector;
                else
                    fc = [];
                end
            else
                % Use the connector explicitly specified by client:
                fc = B.Connector;
            end
            
            % Convert to a BaseConnector object if possible
            %
            if iscell(fc)
                emptyConnector = cellfun(@(x) ~isempty(x),fc);
                if any(emptyConnector) && any(cellfun(@(x) ~isa(x,'PCBConnectors.BaseConnector'),fc(emptyConnector)))
                    try
                        fc = cellfun(@(x) feval(x),fc(emptyConnector));
                    catch me
                        error(message('antenna:antennaerrors:ConnectorTypeNotSupported'));
                    end
                end
            else
                if ~isempty(fc) && ~isa(fc,'PCBConnectors.BaseConnector')
                    try
                        fc = feval(fc);
                    catch me
                        error(message('antenna:antennaerrors:ConnectorTypeNotSupported'));
                    end
                end
            end
            
            % Cache the object for subsequent use
            %  - lifecycle is that it gets reset at start of generate()
            B.CachedConnector = fc; % cache it
            if ~iscell(fc)
                fc = {fc};
            end
        end
        
        function m = getPCBMargin(B)
            % Return PCBMargin in meters if no connector, or if connector
            % object has AllowPCBMargin = true.
            
            fc = getConnector(B);
            
            % Check for connector and AllowPCBMargin true
            if iscell(fc)
                emptyConnector = cellfun(@(x) isempty(x),fc);
                emptyConnectorCheck = all(emptyConnector);
                pcbMargin = all(cellfun(@(x) x.AllowPCBMargin,fc(~emptyConnector)));
            else
                emptyConnectorCheck = isempty(fc);
                pcbMargin = fc.AllowPCBMargin;
            end            
            
            if emptyConnectorCheck || pcbMargin
                m = B.PCBMargin;
            else
                % PCB margin disallowed, force to zero:
                m = 0.0;
            end
        end
        
        function addConnectors(B)
            % Add feedpoint connector
            %
            % Allow for no feedpoint connector
            fc = getConnector(B);
%             if ~iscell(fc)
%                 fc = {fc};
%             end
            connectorIndex = find(cellfun(@(x) ~isempty(x),fc));
            for i = connectorIndex               
                if ~isempty(fc{i})
                    add(fc{i},B,i);
                end
            end
        end
    end
    
    % Board profile
    %
    methods (Access=protected)
        function setCoordPrecision(B)
            % Determine number of integer digits needed from metal layer.
            %  - Set this into Integer part of CoordPrecision in
            %    Gerber.Writer.
            %  - That gets pushed into each ContentGerber instance
            %
            % W.CoordPrecision = [integerDigits fractionDigits]
            
            % Get board profile bounding box, [x y dx dy]
            cacheAntennaBoundingBox(B);
            bb = B.AntennaBoundingBox;
            
            % Determine largest coordinates to be expressed on PCB.
            %
            % Estimate this from the largest absolute coord values of metal
            % bounding box, and add the PCB margins
            x1 = abs(bb(1));
            x2 = abs(bb(1)+bb(3));
            y1 = abs(bb(2));
            y2 = abs(bb(2)+bb(4));
            
            W = B.Writer;
            scale = getUnitsScale(W); % meters to output units
            maxAbsCoord = max([x1 x2 y1 y2]) + getPCBMargin(B)*scale;
            
            % Determine # integer digits needed to represent max coord. We
            % will always have at least 1 integer digit (Gerber spec allows
            % zero).
            Nint = max(1,floor(1+log10(maxAbsCoord)));
            
            % Specify max # fractional digits:
            %u = W.CoordUnits;
            % Max # frac digits is 6 based on Gerber spec sec 4.9.1
            % It's not based on units selected (mm vs in)
            Nfrac = 6;
            
            % Update # integer digits in coordinate setting.
            W.CoordPrecision = [Nint Nfrac];
        end
        
        function addProfile(B)
            % Write PCB board profile, either to a separate file (for
            % Gerber x2) or to the top silkscreen.
            %
            % A "board profile" is a boundary, usual rectangular which we
            % do here but it could be any shape.  It defines the actual
            % board cut using a "closed chain" of lines, where last point
            % is the first point.
            
            % Where to write it:
            %
            W = B.Writer;
            if isGerberExtension(W,2) || strcmpi(W.BoardProfileFile,'profile')
                % Create a "Profile" file
                F = Gerber.Profile.getOrCreate(W);
            else
                % The usual "x1" technique is to add the profile to another
                % file, such as Legend (silkscreen)
                F = Gerber.Legend.getOrCreate(W,'top');
            end
            
            % What to write:
            %
            addBoardProfileToFile(B,F);
        end
        
        function addBoardProfileToFile(B,F)
            % Add board profile as rectangular bounding box to file.
            % Writes PCB dimensioning profile to file F
            %  - Based on extent of antenna design, plus a margin
            %  - Uses BoardProfileLineWidth
            
            % Get board profile, and flag indicating if an additional
            % margin is present
            [pro,hasMargin] = getBoardProfile(B);

            % Define and select acircular aperture to draw board profile:
            C = getContent(F);
            W = B.Writer;
            scale = getUnitsScale(W);
            w = W.BoardProfileLineWidth*25.4e-6 * scale; % convert MILS to output units
            ap = defineAperture(C,'circle',w); % xxx can we reuse prior aperture?
            ap.Comment = 'For board profile'; % comment for aperture
            
            % Write profile line
            msg = 'Board profile';
            if hasMargin
                msg = [msg ', including additional margin'];
            end
            addComment(C,msg); % comment in Gerber text
            selectAperture(C,ap);
            addPolarity(C,'dark');
            addRect(C,pro);
            
            % If there is no metal in design, add a comment to the Profile
            % file indicating this.  It will likely be the ONLY file we
            % write.
            if isempty(B.ScaledDesign)
                str1 = 'Empty PCB - no metal defined';
                str2 = repmat('=',1,numel(str1));
                addComment(C,str2);
                addComment(C,str1);
                addComment(C,str2);
            end
        end
        
        function [pro_O,hasMargin] = getBoardProfile(B,newUnits)
            % getBoardProfile(B) return board profile as a rectangle in
            % output units.  Profile is the bounding box containing all
            % antenna elements (such as copper, vias, feedlocations), plus
            % PCBMargin, and is returned as [x y dx dy].
            %
            % getBoardProfile(B,units) where units = 'in', 'mm' or 'output'
            % specifies the units for the profile dimensions.
            
            % Get antenna extent, NOT including margin
            cacheAntennaBoundingBox(B);
            pro_O = B.AntennaBoundingBox; % output units
            
            % Return extra space beyond board profile required by
            % connector:
            fc = getConnector(B);
            % No connector check
            isConnector = any(cellfun(@(x) ~isempty(x),fc));
            if isConnector
                if iscell(fc)
                    connIndx = cellfun(@(x) ~isempty(x),fc);
                    fc = fc(connIndx);
                    profileSpaceRects = cell2mat(cellfun(@(x) getProfileSpaceRect(x),fc,'UniformOutput',false)');
                    areaRects = profileSpaceRects(:,3).*profileSpaceRects(:,4);
                    [~,Id] = max(areaRects);
                    profileSpaceRect = profileSpaceRects(Id);
                else
                    profileSpaceRect = getProfileSpaceRect(fc);
                end
                if ~isempty(fc)
                    pro_O = pro_O + profileSpaceRect;
                end
            end
            % If we are allowed to add PCB margin, do it.
            %
            % Edge-launch feedpoint connectors don't allow it.
            %
            W = B.Writer;
            m = getPCBMargin(B);
            hasMargin = m > 0;
            if hasMargin
                scale_M2O = getUnitsScale(W); % meters to output units
                d_O = m * scale_M2O; % meters to output units
                pro_O = pro_O + [-d_O -d_O 2*d_O 2*d_O]; % output units
            end
            
            if nargin > 1
                pro_O  = pro_O .* convertOutputUnits(W,newUnits);
            end
        end
    end
    
    % Antenna copper
    %
    methods (Access=private)
        function writeAntennaMetal(B)
            % Write single layer of copper
            %
            % Called by writeGerber(), which updates .ScaledDesign, so we
            % can use .ScaledDesign here:
            if isempty(B.ScaledDesign)
                return
            end
            N = numel(B.ScaledDesign.Layers);
            for i = 1:N
                F = Gerber.Copper.getOrCreate(B.Writer,i,'Signal');
                
                % Add profile to each copper file if needed
                %addBoardProfileToFile(aw,F);
                
                addAntennaMetalToFile(B,F,i);
            end
        end
        
        function y = isMetalSpecified(B)
            % True if any metal is present on any layer.
            
            % Use B.Design here, *not* B.ScaledDesign.
            %
            % That's because .ScaledDesign is only updated when
            % writeGerber() gets called, yet this gets called at any time.
            
            y = false;
            d = B.Design; % Use .Design, *not* .ScaledDesign
            if ~isempty(d)
                % Return true if any *.Layers(i).Fills is not empty
                N = numel(d.Layers);
                for i = 1:N
                    fills_i = d.Layers(i).Fills;
                    if ~isempty(fills_i)
                        % To be completely sure, check for a non-empty
                        % polygon matrix (can be many matrices in a layer)
                        if any(~cellfun(@isempty,fills_i))
                            y = true;
                            return
                        end
                    end
                end
            end
        end
        
        function addAntennaMetalToFile(B,F,i)
            % Write each polygon defined in .ScaledDesign.Layers.Fills(i)
            % and .ScaledDesign.Layers.Holes(i) into the Gerber stack file
            % F.
            %
            % Both values are cells of possibly multiple polygon vertex
            % matrices.
            
            C = getContent(F); % Content object within File

            %Getting fills and holes
            poly_f = B.ScaledDesign.Layers(i).Fills; % cell vector
            M = numel(poly_f);
            poly_h = B.ScaledDesign.Layers(i).Holes; % cell vector
            N = numel(poly_h);

            %If No fills, return
            if (M==0) || ~iscell(poly_f)
                return
            end

            %If No holes, set flag
            if (N==0) || ~iscell(poly_h) || all(cellfun(@isempty,poly_h))
                % {} or not a cell at all
                % {[]} -> nothing
                noHoles = 1;
            else
                noHoles = 0;
            end

            % Render metal fills

            addComment(C,getString(message('antenna:emcatalog:Metallayer',num2str(i))));
            for j = 1:M
                addPolarity(C,'dark');
                % Render polygon j in layer i
                % - Will always be an Nx2 matrix
                addFilledPolygon(C,poly_f{j});
                %addFilledPolygon_OptimizedSameCoords(C,poly_i{j});

                % Render polygon j in layer i
                % - can be an Nx2 matrix
                % - can be a cell of Nx2 matrices,
                %   i.e., multiple holes in one filled polygon

                % Render metal holes corrsponding to respective fills
                if ~noHoles
                    p_ij = poly_h{j};
                    if ~iscell(p_ij)
                        p_ij = {p_ij};
                    end
                    % If no corresponding holes for fill , skip this part
                    if ~all(cellfun(@isempty,p_ij))
                        for k = 1:numel(p_ij)
                            addPolarity(C,'clear');
                            addFilledPolygon(C,p_ij{k});
                            %addFilledPolygon_OptimizedSameCoords(C,p_ij{k});
                        end
                    end   
                end
            end
            
%             % Render metal holes
%             if (M==0) || ~iscell(poly_h)
%                 % {} or not a cell at all
%                 return
%             end
%             if all(cellfun(@isempty,poly_h))
%                 % {[]} -> nothing to do 
%                 return
%             end
%             for j = 1:M
%                 % Render polygon j in layer i
%                 % - can be an Nx2 matrix
%                 % - can be a cell of Nx2 matrices,
%                 %   i.e., multiple holes in one filled polygon
%                 p_ij = poly_h{j};
%                 if ~iscell(p_ij)
%                     p_ij = {p_ij};
%                 end
%                 for k = 1:numel(p_ij)
%                     addPolarity(C,'clear');
%                     addFilledPolygon(C,p_ij{k});
%                     %addFilledPolygon_OptimizedSameCoords(C,p_ij{k});
%                 end
%             end
        end
        
        function findAntennaBoundingBox(B)
            % Sets AntennaBoundingBox to the rectangle that tightly encloses
            % the union of relevant antenna-specific design specs across
            % all layers:
            %  - Layers (copper)
            %  - ViaLocations
            %  - FeedLocations
            %
            % Bounding box stored in OUTPUT UNITS (scaled).
            
            if isempty(B.ScaledDesign)
                B.AntennaBoundingBox = [0 0 0 0];
                return % no data -> no bounding box
            end
            
            % Preset extent:
            xy_min = [ inf  inf];
            xy_max = [-inf -inf];
            
            % Extent of .Layers(:).Fills
            %
            N = numel(B.ScaledDesign.Layers);
            for i = 1:N % layer i
                % Determine extent of metal on this layer:
                poly_i = B.ScaledDesign.Layers(i).Fills;
                
                M = numel(poly_i);
                for j = 1:M % polygon j in layer i
                    p_ij = poly_i{j}; % Nx2 matrix of 2D coords
                    
                    if size(p_ij,2) ~= 2
                        error(message('antenna:emcatalog:PCBWriter',num2str(i),num2str(j)));
                            
                    end
                    
                    this_xy_min = min(p_ij,[],1); % min of cols [x y]
                    this_xy_max = max(p_ij,[],1); % max of cols [x y]
                    xy_min = min(xy_min,this_xy_min);
                    xy_max = max(xy_max,this_xy_max);
                end
            end
            
            % Extent of .ViaLocations
            %
            % Plated vias represent cross-layer shorts.
            % These are specified in the antenna geometry data as:
            %   .ViaLocations = [x y fromLayer toLayer]
            % or
            %   .ViaLocations = {[x y fromLayer toLayer], ...}
            %
            % xy positions specified in current units (mm or in)
            vias = B.ScaledDesign.ViaLocations;
            if ~iscell(vias)
                vias = {vias};
            end
            N = numel(vias);
            for i = 1:N
                v_i = vias{i}; % [x y fromLayer toLayer]
                if ~isempty(v_i)
                    % No via if layers are the same (v_i(3:4))
                    if v_i(3) ~= v_i(4)
                        % There is a cross-layer plated via hole at (x,y)
                        this_xy = v_i(1:2);
                        xy_min = min(xy_min,this_xy);
                        xy_max = max(xy_max,this_xy);
                    end
                end
            end
            
            % FeedLocations is a cell with 3-element vectors,
            %   {[x y layer], [x y layer], ...}
            %
            feeds = B.ScaledDesign.FeedLocations;
            if ~iscell(feeds)
                feeds = {feeds};
            end
            N = numel(feeds);
            for i = 1:N
                v_i = feeds{i}; % [x y layer]
                if ~isempty(v_i)
                    this_xy = v_i(1:2);
                    xy_min = min(xy_min,this_xy);
                    xy_max = max(xy_max,this_xy);
                end
            end
            
            % Record bounding box: [x y dx dy]
            %
            B.AntennaBoundingBox = [xy_min xy_max-xy_min];
        end
    end
    
    methods (Access = ?PCBConnectors.BaseConnector)
        function cacheAntennaBoundingBox(B)
            % Compute and store antenna bounding box in OUTPUT UNITS, if
            % not yet stored.
            %
            % This does NOT include an RF connector or PCB margin, just
            % the physical antenna elements.
            
            if isempty(B.AntennaBoundingBox)
                findAntennaBoundingBox(B);
            end
        end
    end
    
    % Via file
    %
    methods (Access=private)
        function addExplicitVias(B)
            % Add explicit plated vias that represents cross-layer shorts.
            % These are specified in the antenna geometry data as:
            %   .ViaLocations = [x y fromLayer toLayer]
            % or
            %   .ViaLocations = {[x y fromLayer toLayer], ...}
            %
            % xy positions specified in current units (mm or in)
            
            % Info regarding PCB hole sizes:
            % http://www.eurocircuits.com/drilled-holes-in-the-pcb-
            
            % No antenna geometry data?
            if isempty(B.ScaledDesign)
                return
            end
            W = B.Writer;
            addVia(W, B.ScaledDesign.ViaLocations, B.Design.ViaDiameter);
        end
    end
    
    % Soldermask support
    %
    methods (Access=private)
        function addSoldermask(B)
            % Ensure soldermask gets added to top and/or bottom of PCB,
            % based on property setting.
            
            if isempty(B.ScaledDesign)
                return
            end
            
            % Just the creation/presence of a top or bottom soldermask file
            % ensures the mask is added.  Adding holes within the mask
            % is the responsibility of other methods while they carry out
            % their other tasks.
            %
            % All we need to do here is create the top and/or bottom file
            % if it is requested - no LHS return args are needed.
            % Basically, this ensures that even if no other method forces a
            % hole written to the mask, we still get the full mask.
            getSoldermaskFiles(B);
        end
    end
    
    methods (Access = ?PCBConnectors.BaseConnector)
        function [Ft,Fb] = getSoldermaskFiles(B)
            % Creates soldermask files if not present
            %
            % Respects option to omit soldermask from top, bottom or both.
            % This returns empty when top/bottom not desired.
            
            W = B.Writer;
            if any(strcmpi(B.Soldermask,{'top','both'}))
                Ft = Gerber.Soldermask.getOrCreate(W,'top');
            else
                Ft = [];
            end
            if any(strcmpi(B.Soldermask,{'bottom','both'}))
                Fb = Gerber.Soldermask.getOrCreate(W,'bottom');
            else
                Fb = [];
            end
        end
    end
    
    % Design Info
    %
    methods (Access=private)
        function addDesignInfo(B)
            % Add general design info text to TOP silkscreen, positioned
            % BELOW the bottom of the board profile. This will display when
            % outline is ignored. This will NOT appear in finished PCB.
            %
            % Add connector info to BOTTOM silkscreen.
            
            if isempty(B.ScaledDesign)
                return
            end
            W = B.Writer;
            Ft = Gerber.Legend.getOrCreate(W,'top');
            Ct = getContent(Ft);
            Fb = Gerber.Legend.getOrCreate(W,'bottom');
            Cb = getContent(Fb);
            
            % Need units of inches for text placement:
            %
            font = B.Font;
            [siz_I,siz_Pts] = getInfoFontSize(B);
            
            % First text line is one line below bounding box.
            % Initialize to one line ABOVE where text will begin.
            dy = 1.2 * siz_I;  % height for one line of text
            
            % pro_I is mfg board profile, pro_I(1:2) is bottom left coord
            pro_I = getBoardProfile(B,'in'); % [x y dx dy], INCHES
            
            % Horizontal line requires an aperture to draw boundary line
            % with prescribed line width.
            %
            % Convert line width from MILS (1/1000") to meters, then to
            % output units:
            scale = getUnitsScale(W);
            lwid = B.DesignInfoSeparationLineWidth * 25.4e-6 * scale;
            
            % Render partition line to TOP and BOTTOM
            %
            % CAREFUL: the line cannot extend over exposed copper.
            %   Bottom edge connectors have exposed copper right at the
            %   bottom edge of the PCB.  So ensure the full line width of
            %   the partition line is PAST the board edge.  Lines are drawn
            %   at the midpt of the aperture width.
            %
            pro = getBoardProfile(B); % output units
            tmp = [pro(1)        pro(2)-lwid/2; ...
                   pro(1)+pro(3) pro(2)-lwid/2];
            
            ap = defineAperture(Ct,'circle',lwid); % diameter
            ap.Comment = 'Design info boundary';
            selectAperture(Ct,ap);
            addPolyline(Ct,tmp,'dark');
            
            ap = defineAperture(Cb,'circle',lwid); % diameter
            ap.Comment = 'Design info boundary';
            selectAperture(Cb,ap);
            addPolyline(Cb,tmp,'dark');
            
            % Render MATLAB logo
            % - icon is in MAT file as variable 'bw'
            % - render at bottom right of top and bottom layers
            % - full height of info region
            %
            % Top layer:
            % VI: Use DesignInfo fields set to empty to draw this
            if isempty(B.ScaledDesign.DesignInfo) || ~isempty(B.ScaledDesign.DesignInfo.Title)
                NL = 3; % # lines of text in design info region
                iconFile = [matlabroot '/toolbox/shared/em_catalog/+em/+internal/+cadsupport/matlab_logo.mat'];
                siz_pts = floor(dy*NL*72);
                xy_ico = [pro_I(1)+pro_I(3), pro_I(2)];
                addText(Ct,iconFile,...
                    'IsFile',true, ...
                    'FontSize',siz_pts, ...
                    'Position',xy_ico, ...
                    'Anchor','topright', ...
                    'Mirror',false);
           
            %
            % Bottom layer:
            xy_ico = pro_I(1:2);
            addText(Cb,iconFile,...
                'IsFile',true, ...
                'FontSize',siz_pts, ...
                'Position',xy_ico, ...
                'Anchor','topleft', ...
                'Mirror',true);
            end
            % Top design info
            %
            txt = getDesignInfoText(B);
            mirror = false;
            xy = pro_I(1:2);
            if ~isempty(txt.Title)
                xy = xy - [0 dy];
                addText(Ct,txt.Title, ...
                    'FontSize',siz_Pts, ...
                    'Position',xy, ...
                    'Anchor','bottomleft', ...
                    'Mirror',mirror, ...
                    'Font',font);
            end
            if ~isempty(txt.Comment)
                xy = xy - [0 dy];
                addText(Ct,txt.Comment, ...
                    'FontSize',siz_Pts, ...
                    'Position',xy, ...
                    'Anchor','bottomleft', ...
                    'Mirror',mirror, ...
                    'Font',font);
            end
            if ~isempty(txt.Attribute)
                xy = xy - [0 dy];
                addText(Ct,txt.Attribute, ...
                    'FontSize',siz_Pts, ...
                    'Position',xy, ...
                    'Anchor','bottomleft', ...
                    'Mirror',mirror, ...
                    'Font',font);
            end
            
            % Render BOTTOM design info
            %
            fc = getConnector(B);
            if isequal(numel(fc),1)
                fc = fc{1};
                mirror = true;
                xy = pro_I(1:2) + [pro_I(3) 0];
                
                % Check for empty connector and type
                if ~isempty(fc)
                    if iscell(fc)
                        emptyConnector = all(cellfun(@(x) isempty(x),fc));
                        emptyType = all(cellfun(@(x) isempty(x.Type),fc));
                        
                    else
                        emptyConnector = isempty(fc);
                        emptyType = isempty(fc.Type);
                    end
                else
                    emptyConnector = true;
                end
                
                if emptyConnector || emptyType
                    xy = xy - [0 dy];
                    addText(Cb,'No connector', ...
                        'FontSize',siz_Pts, ...
                        'Position',xy, ...
                        'Anchor','bottomright', ...
                        'Mirror',mirror, ...
                        'Font',font);
                else
                    ohm = 'ohm';
                    %ohm = native2unicode([226 132 166],'utf-8');
                    str = sprintf('%s %g%s',fc.Type,fc.Impedance,ohm);
                    
                    xy = xy - [0 dy];
                    addText(Cb,str, ...
                        'FontSize',siz_Pts, ...
                        'Position',xy, ...
                        'Anchor','bottomright', ...
                        'Mirror',mirror, ...
                        'Font',font);
                    
                    xy = xy - [0 dy];
                    addText(Cb,[fc.Mfg ' #' fc.Part], ...
                        'FontSize',siz_Pts, ...
                        'Position',xy, ...
                        'Anchor','bottomright', ...
                        'Mirror',mirror, ...
                        'Font',font);
                end
                
                % Remove solder mask from "info" section outside of board profile
                xy = xy - [0 dy]; % next line
                removeSoldermaskFromDesignInfo(B,xy);
            end

        end
        
        function txt = getDesignInfoText(B)
            % Get text comments from ScaledDesign.DesignInfo.
            % Assumes the field names:
            %   .Title     - Name of design, purpose or antenna type.
            %                Ex: 'Planar patch'
            %                Default: '' (empty)
            %   .Comment   - Key specifications or performance info.
            %                Ex: '+1.2dB @ 6.0 GHz'
            %                Default: 'Designed with Antenna Toolbox'
            %   .Attribute - Adds generation timestamp if not present.
            %                Default: 'Generated 2016-Jan-01 12:00'
            
            d = B.ScaledDesign;
            if isfield(d,'DesignInfo')
                txt = B.ScaledDesign.DesignInfo;
            else
                txt = [];
            end
            
            if ~isfield(txt,'Title')
                txt.Title = '';
            end
            if ~isfield(txt,'Comment')
                if (~isfield(d,'Source'))||strcmpi(d.Source,'pcbStack')
                    txt.Comment = 'Designed with Antenna Toolbox';
                elseif strcmpi(d.Source, 'pcbComponent')
                    txt.Comment = 'Designed with RF PCB Toolbox';
                end
            end
            if ~isfield(txt,'Attribute')
                txt.Attribute = [getString(message('antenna:emcatalog:PCBWriterGen')) datestr(now,'yyyy-mmm-dd HH:MM')];
            end
        end

        function removeSoldermaskFromDesignInfo(B,xy)
            % Clear out TOP soldermask from rectangular info section.
            %
            % xy is the bottom-left corner of all info text.
            %
            % Only specify this if top soldermask file is being generated.
            % Otherwise we accidentally add a full soldermask to the top of
            % the board!
            
            if ~any(strcmpi(B.Soldermask,{'none','neither'}))
                % Board profile (not including info region)
                p = getBoardProfile(B); % [x y dx dy], in output units
                W = B.Writer;
                %fix for xy in 'mm' units g2807056
                if strcmpi(W.CoordUnits,'mm')
                    xy = xy*25.4;
                end
                dy_text = p(2) - xy(2);
                
                x = p(1);
                y = xy(2);
                dx = p(3);
                dy = dy_text;
                rect = [x y dx dy];
                
                % Remove from front and back, based on soldermask property
                %
                if any(strcmpi(B.Soldermask,{'top','both'}))
                    F = findFile(W,Gerber.Soldermask.Use,'top',1);
                    if ~isempty(F)
                        C = getContent(F);
                        addFilledRect(C,rect);
                    end
                end
                if any(strcmpi(B.Soldermask,{'bottom','both'}))
                    F = findFile(W,Gerber.Soldermask.Use,'bottom',1);
                    if ~isempty(F)
                        C = getContent(F);
                        addFilledRect(C,rect);
                    end
                end
            end
        end
    end
    
    methods (Access = {?PCBConnectors.BaseConnector})
        function [siz_I,siz_Pts] = getInfoFontSize(B)
            % Return font size in points for rendering Info text, in
            % INCHES.
            %
            % Returns board profile in INCHES.
            %
            % Goal is a desired number of lines of text in a region that is
            % equal in height to the active PCB region, with constraint on
            % minimum and maximum font size.
            
            siz_Pts = B.DesignInfoFontSize;
            if isempty(siz_Pts)
                % Auto font size:
                p = getBoardProfile(B,'in'); % [x y dx dy], INCHES
                h = p(4); % height of PCB active region in INCHES
                
                % Return desired height in INCHES.
                % Min and max are in MILS (1/1000")
                siz_I = max(B.MinInfoFontSize/1000, ...
                    min(B.MaxInfoFontSize/1000, ...
                    h ./ B.DesiredLinesOfFontInfo));
                
                siz_Pts = siz_I * 72;
            else
                % Manual font size:
                siz_I = siz_Pts / 72; % inch units
            end
        end
    end
    
    % Antenna MAT file support
    %
    methods
        function load(B,arg)
            %load Load a multi-layer 2D antenna design.
            %   load(B,D) loads a multi-layer 2D antenna design from a
            %   variable.
            %
            %   load(B,FILENAME) loads a multi-layer 2D antenna design from
            %   the MAT file FILENAME.  FILENAME is a string containing the
            %   name of the file to be loaded.  If no extension is
            %   specified, '.mat' is assumed.
            %
            %   After it is loaded, the antenna design can be accessed from
            %   the Design property.  See help for a full specification of
            %   the antenna design data structure.
            %
            %   See also Design, PCBWriter, generate.
            
            % When loading a new design, remove all cached data
            resetCache(B);
            
            if ischar(arg)
                % MAT file name passed
                % Check filename, add '.mat' if needed
                [p,n,e] = fileparts(arg);
                if isempty(e)
                    e = '.mat';
                end
                fname = fullfile(p,[n e]);
                s = load(fname);
            else
                % Design variable passed
                s = arg;
            end
            B.Design = s; % checks syntax via set method
        end
        
        function removeDefaultDesignInfo(B)
            dinfo = B.Design.DesignInfo;
            dinfo.Title = [];
            dinfo.Comment = [];
            dinfo.Attribute = [];
            B.Design.DesignInfo = dinfo;            
        end
    end
    
    methods (Access=private)
        function checkDataStruct(aw,data)
            if ~isempty(data) && ~hasGeomFields(aw,data)
                error(message('antenna:antennaerrors:InvalidAntennaGeomStruct'));
            end
        end
        
        function rescaleDesign(B)
            % Convert coordinates from meters to mm or in.
            %
            % Imported data is assumed to be in units of METERS. Convert to
            % units specified in Gerber.Writer property.
            
            % Data has already been checked prior to this call for all
            % required fields.  It could be empty however:
            d = B.Design;
            if isempty(d)
                B.ScaledDesign = [];
                return % no data
            end
            
            % Get scale factor that converts meters to output units set on
            % the Gerber.Writer:
            scale = getUnitsScale(B.Writer);
            
            % FeedLocations
            %
            % cell-vector w/zero or more vectors specifying [x y Layer]:
            N = numel(d.FeedLocations);
            for i = 1:N
                f_i = d.FeedLocations{i};
                if ~isempty(f_i)
                    d.FeedLocations{i} = [f_i(1:2)*scale f_i(3:end)]; %f_i(3:end) ?? why only 3
                end
            end
            
            % ViaLocations
            %
            % similar, with [x y fromLayer toLayer]
            N = numel(d.ViaLocations);
            for i = 1:N
                f_i = d.ViaLocations{i};
                if ~isempty(f_i)
                    d.ViaLocations{i} = [f_i(1:2)*scale f_i(3:4)];
                end
            end

            if ~isempty(d.ViaDiameter)
                d.ViaDiameter = scale*d.ViaDiameter;
            end
            
            % Layers
            %
            N = numel(d.Layers);
            for i = 1:N % loop over layers
                layer_i = d.Layers(i);

                % Just one substrate per layer, scalar value:
                if ~isempty(layer_i.Substrate)
                    layer_i.Substrate = scale * layer_i.Substrate;
                end
                
                % There are P Fills in a cell vector
                %   - at least one Nx2 matrix
                % There may be 1 or more holes for each Fill
                %   - can be empty matrix
                %   - can be Nx2 matrix
                %   - can be empty cell
                %   - can be cell with empty
                %   - can be cell with one or more Nx2 matrices
                P = numel(layer_i.Fills);
                for j = 1:P % loop over each fill region
                    fills_j = layer_i.Fills{j};
                    holes_j = layer_i.Holes{j};
                    
                    if isempty(fills_j)
                        % Unusual, but valid - just move on to next fill,
                        % after ensuring nothing bad is happening (i.e.,
                        % holes without fills):
                        if ~isempty(holes_j)
                            error(message('antenna:emcatalog:emptyFillserror'));
                        end
                        continue
                    end
                    
                    % Fills: empty or one matrix
                    layer_i.Fills{j} = scale * fills_j;
                    
                    % More complicated for Holes:
                    if ~isempty(holes_j)
                        if iscell(holes_j)
                            % multiple holes in this fill, contained in a
                            % cell-vector
                            Q = numel(holes_j); % length of cell vector
                            for k = 1:Q
                                holes_j{k} = scale * holes_j{k};
                            end
                        else
                            % one hole in this fill, no cell-vector of holes
                            %
                            % could be empty matrix:
                            holes_j = scale * holes_j;
                        end
                        layer_i.Holes{j} = holes_j;
                    end
                end
                
                % Update before moving to next layer
                d.Layers(i) = layer_i;
            end
            
            % Store scaled data:
            B.ScaledDesign = d;
        end
        
        function y = hasGeomFields(aw,s)
            % Return true if s is a struct containing at least all
            % fieldnames required for an Antenna Geometry structure.
            
            % Test struct field names:
            % - all strings in ReqdAntFields must appear in s
            % - all strings in ReqdLayersFields must appear in s.Layers
            % - any fields in OptTopFields may appear in s;
            %   none are checked/tested here
            y = isstruct(s) && ...
                all(ismember(aw.ReqdTopFields,fieldnames(s))) && ...
                isstruct(s.Layers) && ...
                all(ismember(aw.ReqdLayersFields,fieldnames(s.Layers)));
        end
    end
end
