765 lines
35 KiB
Python
765 lines
35 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2022-2023 Salvador E. Tropea
|
|
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
|
|
# License: GPL-3.0
|
|
# Project: KiBot (formerly KiPlot)
|
|
"""
|
|
Dependencies:
|
|
- from: KiKit
|
|
role: mandatory
|
|
"""
|
|
import collections
|
|
from copy import deepcopy
|
|
import os
|
|
import re
|
|
import json
|
|
from tempfile import NamedTemporaryFile
|
|
from .error import KiPlotConfigurationError
|
|
from .gs import GS
|
|
from .kiplot import run_command, config_output, register_xmp_import
|
|
from .layer import Layer
|
|
from .misc import W_PANELEMPTY, KIKIT_UNIT_ALIASES, W_KEEPTMP
|
|
from .optionable import PanelOptions
|
|
from .out_base import VariantOptions
|
|
from .registrable import RegOutput
|
|
from .macros import macros, document, output_class # noqa: F401
|
|
from . import log
|
|
|
|
logger = log.get_logger()
|
|
|
|
|
|
def update_dict(d, u):
|
|
for k, v in u.items():
|
|
if isinstance(v, collections.abc.Mapping):
|
|
d[k] = update_dict(d.get(k, {}), v)
|
|
elif isinstance(v, list) and k in d:
|
|
d[k] = v+d[k]
|
|
else:
|
|
d[k] = v
|
|
return d
|
|
|
|
|
|
class PanelOptionsWithPlugin(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.code = ''
|
|
""" Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin* """
|
|
self.arg = ''
|
|
""" Argument to pass to the plugin. Used for *plugin* """
|
|
super().__init__()
|
|
|
|
|
|
class PanelizePage(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'inherit'
|
|
""" *[inherit,custom,A0,A1,A2,A3,A4,A5,A,B,C,D,E,USLetter,USLegal,USLedger,A0-portrait,A1-portrait,A2-portrait,
|
|
A3-portrait,A4-portrait,A5-portrait,A-portrait,B-portrait,C-portrait,D-portrait,E-portrait,
|
|
USLetter-portrait,USLegal-portrait,USLedger-portrait] Paper size. The default `inherit` option inherits
|
|
paper size from the source board. This feature is not supported on KiCAD 5 """
|
|
self.page_size = None
|
|
""" {type} """
|
|
self.size = None
|
|
""" {type} """
|
|
self.anchor = 'tl'
|
|
""" [tl,tr,bl,br,mt,mb,ml,mr,c] Point of the panel to be placed at given position. Can be one of tl, tr, bl, br
|
|
(corners), mt, mb, ml, mr (middle of sides), c (center). The anchors refer to the panel outline """
|
|
self.posx = 15
|
|
""" [number|string] The X position of the panel on the page """
|
|
self.pos_x = None
|
|
""" {posx} """
|
|
self.posy = 15
|
|
""" [number|string] The Y position of the panel on the page """
|
|
self.pos_y = None
|
|
""" {posy} """
|
|
self.width = 297
|
|
""" [number|string] Width for the `custom` paper size """
|
|
self.height = 210
|
|
""" [number|string] Height for the `custom` paper size """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('posx', 'posy', 'width', 'height'))
|
|
|
|
|
|
class PanelizeLayout(PanelOptionsWithPlugin):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'grid'
|
|
""" [grid,plugin] In the plugin type only `code` and `arg` are relevant """
|
|
self.hspace = 0
|
|
""" [number|string] Specify the horizontal gap between the boards """
|
|
self.vspace = 0
|
|
""" [number|string] Specify the vertical gap between the boards """
|
|
self.space = None
|
|
""" [number|string] Specify the gap between the boards, overwrites `hspace` and `vspace` """
|
|
self.rotation = 0
|
|
""" [number|string] Rotate the boards before placing them in the panel """
|
|
self.renamenet = 'Board_{n}-{orig}'
|
|
""" A pattern by which to rename the nets. You can use {n} and {orig} to get the board number and original name """
|
|
self.rename_net = None
|
|
""" {renamenet} """
|
|
self.renameref = '{orig}'
|
|
""" A pattern by which to rename the references. You can use {n} and {orig} to get the board number and original
|
|
name """
|
|
self.rename_ref = None
|
|
""" {renameref} """
|
|
self.baketext = True
|
|
""" A flag that indicates if text variables should be substituted or not """
|
|
self.bake_text = None
|
|
""" {baketext} """
|
|
self.rows = 1
|
|
""" *Specify the number of rows of boards in the grid pattern """
|
|
self.cols = 1
|
|
""" *Specify the number of columns of boards in the grid pattern """
|
|
self.alternation = 'none'
|
|
""" [none,rows,cols,rowsCols] Specify alternations of board rotation.
|
|
none: Do not alternate.
|
|
rows: Rotate boards by 180° on every next row.
|
|
cols: Rotate boards by 180° on every next column.
|
|
rowsCols: Rotate boards by 180° based on a chessboard pattern """
|
|
self.vbackbone = 0
|
|
""" [number|string] The width of vertical backbone (0 means no backbone). The backbone does not increase the
|
|
spacing of the boards """
|
|
self.hbackbone = 0
|
|
""" [number|string] The width of horizontal backbone (0 means no backbone). The backbone does not increase the
|
|
spacing of the boards """
|
|
self.vboneskip = 0
|
|
""" Skip every n vertical backbones. I.e., 1 means place only every other backbone """
|
|
self.hboneskip = 0
|
|
""" Skip every n horizontal backbones. I.e., 1 means place only every other backbone """
|
|
self.vbonecut = True
|
|
""" If there are both backbones specified, specifies if there should be a vertical cut where the backbones
|
|
cross """
|
|
self.hbonecut = True
|
|
""" If there are both backbones specified, specifies if there should be a horizontal cut where the backbones
|
|
cross """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
if self.space:
|
|
self.hspace = self.vspace = self.space
|
|
self.add_units(('vbackbone', 'hbackbone', 'hspace', 'vspace', 'space'))
|
|
self.add_angle(('rotation', ))
|
|
|
|
|
|
class PanelizeTabs(PanelOptionsWithPlugin):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'spacing'
|
|
""" *[fixed,spacing,full,annotation,plugin] Fixed: Place given number of tabs on the PCB edge.
|
|
Spacing: Place tabs on the PCB edges based on spacing.
|
|
Full: Create tabs that are full width of the PCB.
|
|
Corner: Create tabs in the corners of the PCB.
|
|
Annotation: Add tabs based on PCB annotations.
|
|
Plugin: Uses an external python function, only `code` and `arg` are relevant """
|
|
self.vwidth = 3
|
|
""" [number|string] The width of tabs in the vertical direction. Used for *fixed* and *spacing* """
|
|
self.hwidth = 3
|
|
""" [number|string] The width of tabs in the horizontal direction. Used for *fixed* and *spacing* """
|
|
self.width = None
|
|
""" [number|string] The width of tabs in both directions. Overrides both `vwidth` and `hwidth`.
|
|
Used for *fixed*, *spacing*, *corner* and *annotation* """
|
|
self.vcount = 1
|
|
""" Number of tabs in the vertical direction. Used for *fixed* """
|
|
self.hcount = 1
|
|
""" Number of tabs in the horizontal direction. Used for *fixed* """
|
|
self.mindistance = 0
|
|
""" [number|string] Minimal spacing between the tabs. If there are too many tabs, their count is reduced.
|
|
Used for *fixed* """
|
|
self.min_distance = None
|
|
""" {mindistance} """
|
|
self.spacing = 10
|
|
""" [number|string] The maximum spacing of the tabs. Used for *spacing* """
|
|
self.cutout = 1
|
|
""" [number|string] When your design features open pockets on the side, this parameter specifies extra cutout
|
|
depth in order to ensure that a sharp corner of the pocket can be milled. Used for *full* """
|
|
self.tabfootprints = 'kikit:Tab'
|
|
""" The footprint/s used for the *annotation* type. You can specify a list of footprints separated by comma """
|
|
self.tab_footprints = None
|
|
""" {tabfootprints} """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
if self.width:
|
|
self.vwidth = self.hwidth = self.width
|
|
self.add_units(('vwidth', 'hwidth', 'width', 'mindistance', 'spacing', 'cutout'))
|
|
|
|
|
|
class PanelizeCuts(PanelOptionsWithPlugin):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,mousebites,vcuts,layer,plugin] Layer: When KiKit reports it cannot perform cuts, you can render the cuts
|
|
into a layer with this option to understand what's going on. Shouldn't be used for the final design """
|
|
self.drill = 0.5
|
|
""" [number|string] Drill size used for the *mousebites* """
|
|
self.spacing = 0.8
|
|
""" [number|string] The spacing of the holes used for the *mousebites* """
|
|
self.offset = 0
|
|
""" [number|string] Specify the *mousebites* and *vcuts* offset, positive offset puts the cuts into the board,
|
|
negative puts the cuts into the tabs """
|
|
self.prolong = 0
|
|
""" [number|string] Distance for tangential prolongation of the cuts (to cut through the internal corner fillets
|
|
caused by milling). Used for *mousebites* and *layer* """
|
|
self.clearance = 0
|
|
""" [number|string] Specify clearance for copper around V-cuts """
|
|
self.cutcurves = False
|
|
""" Specify if curves should be approximated by straight cuts (e.g., for cutting tabs on circular boards).
|
|
Used for *vcuts* """
|
|
self.cut_curves = None
|
|
""" {cutcurves} """
|
|
self.layer = 'Cmts.User'
|
|
""" Specify the layer to render V-cuts on. Also used for the *layer* type """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('drill', 'spacing', 'offset', 'prolong', 'clearance'))
|
|
res = Layer.solve(self.layer)
|
|
if len(res) > 1:
|
|
raise KiPlotConfigurationError('Must select only one layer for the V-cuts ({})'.format(self.layer))
|
|
|
|
|
|
class PanelizeFraming(PanelOptionsWithPlugin):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,railstb,railslr,frame,tightframe,plugin] Railstb: Add rails on top and bottom.
|
|
Railslr: Add rails on left and right.
|
|
Frame: Add a frame around the board.
|
|
Tighframe: Add a frame around the board which fills the whole area of the panel -
|
|
the boards have just a milled slot around their perimeter.
|
|
Plugin: Uses an external python function, only `code` and `arg` are relevant """
|
|
self.hspace = 2
|
|
""" [number|string] Specify the horizontal space between PCB and the frame/rail """
|
|
self.vspace = 2
|
|
""" [number|string] Specify the vertical space between PCB and the frame/rail """
|
|
self.space = None
|
|
""" [number|string] Specify the space between PCB and the frame/rail. Overrides `hspace` and `vspace` """
|
|
self.width = 5
|
|
""" [number|string] Specify with of the rails or frame """
|
|
self.fillet = 0
|
|
""" [number|string] Specify radius of fillet frame corners """
|
|
self.chamfer = 0
|
|
""" [number|string] Specify the size of chamfer frame corners """
|
|
self.mintotalheight = 0
|
|
""" [number|string] If needed, add extra material to the rail or frame to meet the minimal requested size.
|
|
Useful for services that require minimal panel size """
|
|
self.min_total_height = None
|
|
""" {mintotalheight} """
|
|
self.mintotalwidth = 0
|
|
""" [number|string] If needed, add extra material to the rail or frame to meet the minimal requested size.
|
|
Useful for services that require minimal panel size """
|
|
self.min_total_width = None
|
|
""" {mintotalwidth} """
|
|
self.cuts = 'both'
|
|
""" [none,both,v,h] Specify whether to add cuts to the corners of the frame for easy removal.
|
|
Used for *frame* """
|
|
self.slotwidth = 2
|
|
""" [number|string] Width of the milled slot for *tightframe* """
|
|
self.slot_width = None
|
|
""" {slotwidth} """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
if self.space:
|
|
self.hspace = self.vspace = self.space
|
|
self.add_units(('hspace', 'vspace', 'space', 'width', 'fillet', 'chamfer', 'mintotalwidth', 'mintotalheight',
|
|
'slotwidth'))
|
|
|
|
|
|
class PanelizeTooling(PanelOptionsWithPlugin):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,3hole,4hole,plugin] Add none, 3 or 4 holes to the (rail/frame of) the panel """
|
|
self.hoffset = 0
|
|
""" [number|string] Horizontal offset from panel edges """
|
|
self.voffset = 0
|
|
""" [number|string] Vertical offset from panel edges """
|
|
self.size = 1.152
|
|
""" [number|string] Diameter of the holes """
|
|
self.paste = False
|
|
""" If True, the holes are included in the paste layer (therefore they appear on the stencil) """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('hoffset', 'voffset', 'size'))
|
|
|
|
|
|
class PanelizeFiducials(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,3fid,4fid,plugin] Add none, 3 or 4 fiducials to the (rail/frame of) the panel """
|
|
self.hoffset = 0
|
|
""" [number|string] Horizontal offset from panel edges """
|
|
self.voffset = 0
|
|
""" [number|string] Vertical offset from panel edges """
|
|
self.coppersize = 1
|
|
""" [number|string] Diameter of the copper spot """
|
|
self.copper_size = None
|
|
""" {coppersize} """
|
|
self.opening = 1
|
|
""" [number|string] Diameter of the solder mask opening """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('hoffset', 'voffset', 'coppersize', 'opening'))
|
|
|
|
|
|
class PanelizeText(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,simple] Currently fixed. BTW: don't ask me about this ridiculous default, is how KiKit works """
|
|
self.text = ''
|
|
""" *The text to be displayed. Note that you can escape ; via \\.
|
|
Available variables in text: *date* formats current date as <year>-<month>-<day>,
|
|
*time24* formats current time in 24-hour format,
|
|
*boardTitle* the title from the source board,
|
|
*boardDate* the date from the source board,
|
|
*boardRevision* the revision from the source board,
|
|
*boardCompany* the company from the source board,
|
|
*boardComment1*-*boardComment9* comments from the source board """
|
|
self.anchor = 'mt'
|
|
""" [tl,tr,bl,br,mt,mb,ml,mr,c] Origin of the text. Can be one of tl, tr, bl, br (corners), mt, mb, ml, mr
|
|
(middle of sides), c (center). The anchors refer to the panel outline """
|
|
self.hoffset = 0
|
|
""" [number|string] Specify the horizontal offset from anchor. Respects KiCAD coordinate system """
|
|
self.voffset = 0
|
|
""" [number|string] Specify the vertical offset from anchor. Respects KiCAD coordinate system """
|
|
self.orientation = 0
|
|
""" [number|string] Specify the orientation (angle) """
|
|
self.width = 1.5
|
|
""" [number|string] Width of the characters (the same parameters as KiCAD uses) """
|
|
self.height = 1.5
|
|
""" [number|string] Height of the characters (the same parameters as KiCAD uses) """
|
|
self.hjustify = 'center'
|
|
""" [left,right,center] Horizontal justification of the text """
|
|
self.vjustify = 'center'
|
|
""" [left,right,center] Vertical justification of the text """
|
|
self.thickness = 0.3
|
|
""" [number|string] Stroke thickness """
|
|
self.layer = 'F.SilkS'
|
|
""" Specify text layer """
|
|
self.plugin = ''
|
|
""" Specify the plugin that provides extra variables for the text """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('hoffset', 'voffset', 'width', 'height', 'thickness'))
|
|
self.add_angle(('orientation', ))
|
|
res = Layer.solve(self.layer)
|
|
if len(res) > 1:
|
|
raise KiPlotConfigurationError('Must select only one layer for the text ({})'.format(self.layer))
|
|
if parent.expand_text:
|
|
self.text = parent.expand_filename_both(self.text, is_sch=False, make_safe=False)
|
|
|
|
|
|
class PanelizeCopperfill(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'none'
|
|
""" *[none,solid,hatched] How to fill non-board areas of the panel with copper """
|
|
self.clearance = 0.5
|
|
""" [number|string] Extra clearance from the board perimeters. Suitable for, e.g., not filling the tabs with
|
|
copper """
|
|
self.layers = 'F.Cu,B.Cu'
|
|
""" [string|list(string)] List of layers to fill. Can be a comma-separated string.
|
|
Using *all* means all external copper layers """
|
|
self.width = 1
|
|
""" [number|string] The width of the hatched strokes """
|
|
self.spacing = 1
|
|
""" [number|string] The space between the hatched strokes """
|
|
self.orientation = 45
|
|
""" [number|string] The orientation of the hatched strokes """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('width', 'spacing', 'clearance'))
|
|
self.add_angle(('orientation', ))
|
|
if not isinstance(self.layers, str) or self.layers != 'all':
|
|
if isinstance(self.layers, str):
|
|
self.layers = self.layers.split(',')
|
|
res = Layer.solve(self.layers)
|
|
self.layers = ','.join([la.layer for la in res])
|
|
|
|
|
|
class PanelizePost(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'auto'
|
|
""" [auto] Currently fixed """
|
|
self.copperfill = False
|
|
""" Fill tabs and frame with copper (e.g., to save etchant or to increase rigidity of flex-PCB panels) """
|
|
self.millradius = 0
|
|
""" [number|string] Simulate the milling operation (add fillets to the internal corners).
|
|
Specify mill radius (usually 1 mm). 0 radius disables the functionality """
|
|
self.mill_radius = None
|
|
""" {millradius} """
|
|
self.reconstructarcs = False
|
|
""" The panelization process works on top of a polygonal representation of the board.
|
|
This options allows to reconstruct the arcs in the design before saving the panel """
|
|
self.reconstruct_arcs = None
|
|
""" {reconstructarcs} """
|
|
self.refillzones = False
|
|
""" Refill the user zones after the panel is build.
|
|
This is only necessary when you want your zones to avoid cuts in panel """
|
|
self.refill_zones = None
|
|
""" {refillzones} """
|
|
self.script = ''
|
|
""" A path to custom Python file. The file should contain a function kikitPostprocess(panel, args) that
|
|
receives the prepared panel as the kikit.panelize.Panel object and the user-supplied arguments as a
|
|
string - see `scriptarg`. The function can make arbitrary changes to the panel - you can append text,
|
|
footprints, alter labels, etc. The function is invoked after the whole panel is constructed
|
|
(including all other postprocessing). If you try to add a functionality for a common fabrication
|
|
houses via scripting, consider submitting PR for KiKit """
|
|
self.scriptarg = ''
|
|
""" An arbitrary string passed to the user post-processing script specified in script """
|
|
self.script_arg = None
|
|
""" {scriptarg} """
|
|
self.origin = 'tl'
|
|
""" [tl,tr,bl,br,mt,mb,ml,mr,c] Specify if the auxiliary origin an grid origin should be placed.
|
|
Can be one of tl, tr, bl, br (corners), mt, mb, ml, mr (middle of sides), c (center).
|
|
Empty string does not changes the origin """
|
|
self.dimensions = False
|
|
""" Draw dimensions with the panel size. """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('millradius',))
|
|
|
|
|
|
class PanelizeDebug(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.drawPartitionLines = False
|
|
""" Draw partition lines """
|
|
self.drawBackboneLines = False
|
|
""" Draw backbone lines """
|
|
self.drawboxes = False
|
|
""" Draw boxes """
|
|
self.trace = False
|
|
""" Trace """
|
|
self.deterministic = False
|
|
""" Deterministic """
|
|
self.drawtabfail = False
|
|
""" Draw tab fail """
|
|
super().__init__()
|
|
|
|
|
|
class PanelizeSource(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.type = 'auto'
|
|
""" *[auto,rectangle,annotation] How we select the area of the PCB used for the panelization.
|
|
*auto* uses all the area reported by KiCad, *rectangle* a specified rectangle and
|
|
*annotation* selects a contour marked by a kikit:Board footprint """
|
|
self.stack = 'inherit'
|
|
""" [inherit,2layer,4layer,6layer] Used to reduce the number of layers used for the panel """
|
|
self.tolerance = 1
|
|
""" [number|string] Extra space around the PCB reported size to be included. Used for *auto* and *annotation* """
|
|
self.tlx = 0
|
|
""" [number|string] Top left X coordinate of the rectangle used. Used for *rectangle* """
|
|
self.tly = 0
|
|
""" [number|string] Top left Y coordinate of the rectangle used. Used for *rectangle* """
|
|
self.brx = 0
|
|
""" [number|string] Bottom right X coordinate of the rectangle used. Used for *rectangle* """
|
|
self.bry = 0
|
|
""" [number|string] Bottom right Y coordinate of the rectangle used. Used for *rectangle* """
|
|
self.ref = ''
|
|
""" Reference for the kikit:Board footprint used to select the contour. Used for *annotation* """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self.add_units(('tolerance', 'tlx', 'tly', 'brx', 'bry'))
|
|
|
|
|
|
class PanelizeConfig(PanelOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.name = ''
|
|
""" A name to identify this configuration. If empty will be the order in the list, starting with 1.
|
|
Don't use just a number or it will be confused as an index """
|
|
self.extends = ''
|
|
""" A configuration to use as base for this one. Use the following format: `OUTPUT_NAME[CFG_NAME]` """
|
|
self.page = PanelizePage
|
|
""" *[dict] Sets page size on the resulting panel and position the panel in the page """
|
|
self.layout = PanelizeLayout
|
|
""" *[dict] Layout used for the panel """
|
|
self.tabs = PanelizeTabs
|
|
""" *[dict] Style of the tabs used to join the PCB copies """
|
|
self.cuts = PanelizeCuts
|
|
""" *[dict] Specify how to perform the cuts on the tabs separating the board """
|
|
self.framing = PanelizeFraming
|
|
""" *[dict] Specify the frame around the boards """
|
|
self.tooling = PanelizeTooling
|
|
""" *[dict] Used to add tooling holes to the (rail/frame of) the panel """
|
|
self.fiducials = PanelizeFiducials
|
|
""" *[dict] Used to add fiducial marks to the (rail/frame of) the panel """
|
|
self.text = PanelizeText
|
|
""" [dict] Used to add text to the panel """
|
|
self.text2 = PanelizeText
|
|
""" [dict] Used to add text to the panel """
|
|
self.text3 = PanelizeText
|
|
""" [dict] Used to add text to the panel """
|
|
self.text4 = PanelizeText
|
|
""" [dict] Used to add text to the panel """
|
|
self.copperfill = PanelizeCopperfill
|
|
""" [dict] Fill non-board areas of the panel with copper """
|
|
self.post = PanelizePost
|
|
""" [dict] Finishing touches to the panel """
|
|
self.debug = PanelizeDebug
|
|
""" [dict] Debug options """
|
|
self.source = PanelizeSource
|
|
""" [dict] Used to adjust details of which part of the PCB is panelized """
|
|
self.expand_text = True
|
|
""" Expand text variables and KiBot %X markers in text objects """
|
|
super().__init__()
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
# Avoid confusing names
|
|
name_is_number = True
|
|
try:
|
|
_ = int(self.name)
|
|
except ValueError:
|
|
name_is_number = False
|
|
if name_is_number:
|
|
raise KiPlotConfigurationError("Don't use a number as name, this can be confused with an index ({})".
|
|
format(self.name))
|
|
# Make None all things not specified
|
|
for k, v in self.get_attrs_gen():
|
|
if isinstance(v, type):
|
|
setattr(self, k, None)
|
|
|
|
|
|
class PanelizeOptions(VariantOptions):
|
|
_extends_regex = re.compile(r'(.+)\[(.+)\]')
|
|
|
|
def __init__(self):
|
|
with document:
|
|
self.output = GS.def_global_output
|
|
""" *Filename for the output (%i=panel, %x=kicad_pcb) """
|
|
self.configs = PanelizeConfig
|
|
""" *[list(dict)|list(string)|string] One or more configurations used to create the panel.
|
|
Use a string to include an external configuration, i.e. `myDefault.json`.
|
|
You can also include a preset using `:name`, i.e. `:vcuts`.
|
|
Use a dict to specify the options using the KiBot YAML file """
|
|
self.title = ''
|
|
""" Text used to replace the sheet title. %VALUE expansions are allowed.
|
|
If it starts with `+` the text is concatenated """
|
|
self.units = 'mm'
|
|
""" [millimeters,inches,mils,mm,cm,dm,m,mil,inch,in] Units used when omitted """
|
|
self.default_angles = 'deg'
|
|
""" [deg,°,rad] Angles used when omitted """
|
|
self.create_preview = False
|
|
""" Use PcbDraw to create a preview of the panel """
|
|
super().__init__()
|
|
self._expand_id = 'panel'
|
|
self._expand_ext = 'kicad_pcb'
|
|
|
|
def solve_cfg_name(self, cfg):
|
|
""" Find the name of a configuration that isn't yet configured """
|
|
name = cfg.get('name')
|
|
if name:
|
|
return name
|
|
return str(self._tree['configs'].index(cfg)+1)
|
|
|
|
def solve_extends(self, tree, level=0, used=None, our_name=None):
|
|
base = tree.get('extends')
|
|
if our_name is None:
|
|
our_name = '{}[{}]'.format(self._parent.name, self.solve_cfg_name(tree))
|
|
if used is None:
|
|
used = {our_name}
|
|
else:
|
|
if our_name in used:
|
|
raise KiPlotConfigurationError('Recursive extends detected in `extends: {}` ({})'.format(base, used))
|
|
used.add(our_name)
|
|
logger.debugl(1, "Extending from "+base)
|
|
# Should be an string
|
|
if not isinstance(base, str):
|
|
raise KiPlotConfigurationError('`extends` must be a string, not {}'.format(type(base)))
|
|
# Extract the output and config names
|
|
m = PanelizeOptions._extends_regex.match(base)
|
|
if m is None:
|
|
raise KiPlotConfigurationError('Malformed `extends` reference: `{}` use OUTPUT_NAME[CFG_NAME]'.format(base))
|
|
out_name, cfg_name = m.groups()
|
|
# Look for the output
|
|
out = RegOutput.get_output(out_name)
|
|
if out is None:
|
|
raise KiPlotConfigurationError('Unknown output `{}` in `extends: {}`'.format(out_name, base))
|
|
# Look for the config
|
|
configs = None
|
|
out_options = out._tree.get('options')
|
|
if out_options:
|
|
configs = out_options.get('configs')
|
|
if configs is None or isinstance(configs, str):
|
|
raise KiPlotConfigurationError("Using `extends: {}` but `{}` hasn't configs to copy". format(base, out_name))
|
|
cfg_name_is_number = True
|
|
try:
|
|
id = int(cfg_name)-1
|
|
except ValueError:
|
|
cfg_name_is_number = False
|
|
if cfg_name_is_number:
|
|
# Using an index, is it valid?
|
|
if id >= len(configs):
|
|
raise KiPlotConfigurationError('Using `extends: {}` but `{}` has {} configs'.
|
|
format(base, out_name, len(configs)))
|
|
origin = configs[id]
|
|
else:
|
|
# Using a name
|
|
origin = next(filter(lambda x: 'name' in x and x['name'] == cfg_name, configs), None)
|
|
if origin is None:
|
|
raise KiPlotConfigurationError("Using `extends: {}` but `{}` doesn't define `{}`".
|
|
format(base, out_name, cfg_name))
|
|
# Now we have the origin
|
|
# Does it also use extends?
|
|
origin_extends = origin.get('extends')
|
|
if origin_extends:
|
|
origin = self.solve_extends(origin, level=level+1, used=used, our_name=base)
|
|
# Copy the origin, update it and replace the current values
|
|
logger.debugl(1, "{} before applying {}: {}".format(our_name, base, tree))
|
|
logger.debugl(1, "- Should add {}".format(origin))
|
|
new_origin = deepcopy(origin)
|
|
update_dict(new_origin, tree)
|
|
if level:
|
|
tree = deepcopy(tree)
|
|
update_dict(tree, new_origin)
|
|
if not level:
|
|
# Remove the extends, we solved it
|
|
del tree['extends']
|
|
logger.debugl(1, "After apply: {}".format(tree))
|
|
return tree
|
|
|
|
def config(self, parent):
|
|
self._parent = parent
|
|
# Look for configs that uses extends
|
|
configs = self._tree.get('configs')
|
|
if configs:
|
|
list(map(self.solve_extends, filter(lambda x: 'extends' in x, configs)))
|
|
super().config(parent)
|
|
self.units = KIKIT_UNIT_ALIASES.get(self.units, self.units)
|
|
if isinstance(self.configs, type):
|
|
logger.warning(W_PANELEMPTY+'Generating a panel with default options, not very useful')
|
|
self.configs = []
|
|
elif isinstance(self.configs, str):
|
|
self.configs = [self.configs]
|
|
for c, cfg in enumerate(self.configs):
|
|
if not cfg.name:
|
|
cfg.name = str(c+1)
|
|
|
|
def create_config(self, cfg):
|
|
with NamedTemporaryFile(mode='w', delete=False, suffix='.json', prefix='kibot_panel_cfg') as f:
|
|
logger.debug('Writing panel config to '+f.name)
|
|
cfg_d = {}
|
|
for k, v in cfg.get_attrs_gen():
|
|
if isinstance(v, PanelOptions):
|
|
cfg_d[k] = {ky: va for ky, va in v.get_attrs_gen() if va is not None and v.get_user_defined(ky)}
|
|
js = json.dumps(cfg_d, indent=4)
|
|
logger.debugl(1, js)
|
|
f.write(js)
|
|
return f.name
|
|
|
|
def create_preview_file(self, name):
|
|
if not self.create_preview or not os.path.isfile(name):
|
|
return
|
|
img_name = os.path.splitext(name)[0]+'.png'
|
|
tree = {'name': '_temporal_pcbdraw_preview',
|
|
'type': 'pcbdraw',
|
|
'comment': 'Internally created for panel preview',
|
|
'options': {'output': img_name, 'variant': '', 'format': 'png'}}
|
|
out = RegOutput.get_class_for('pcbdraw')()
|
|
out.set_tree(tree)
|
|
config_output(out)
|
|
logger.debug('Loading PCB panel ...')
|
|
board = GS.load_board_low_level(name)
|
|
logger.debug('Creating preview image ...')
|
|
out.options.create_image(img_name, board)
|
|
# KiCad loads the project automagically, so now we have the wrong project loaded
|
|
# We need to unload the current project to load the new one
|
|
# But we also need to reload the PCB, this is ridiculous ...
|
|
GS.reload_project(GS.pro_file)
|
|
|
|
def run(self, output):
|
|
cmd_kikit, version = self.ensure_tool_get_ver('KiKit')
|
|
if GS.ki5 and version >= (1, 1, 0):
|
|
raise KiPlotConfigurationError("Installed KiKit doesn't support KiCad 5")
|
|
super().run(output)
|
|
fname = self.save_tmp_board_if_variant(new_title=self.title, do_3D=True)
|
|
# Create the command
|
|
cmd = [cmd_kikit, 'panelize'] # , '--dump', 'test.json'
|
|
# Add all the configurations
|
|
for cfg in self.configs:
|
|
cmd.append('--preset')
|
|
if isinstance(cfg, str):
|
|
if cfg[0] != ':' and not os.path.isfile(cfg):
|
|
raise KiPlotConfigurationError('Missing config file: '+cfg)
|
|
cmd.append(cfg)
|
|
else:
|
|
cfg_f = self.create_config(cfg)
|
|
self._files_to_remove.append(cfg_f)
|
|
cmd.append(cfg_f)
|
|
# Add the PCB and output
|
|
cmd.append(fname)
|
|
cmd.append(output)
|
|
remove_tmps = False
|
|
try:
|
|
run_command(cmd)
|
|
self.create_preview_file(output)
|
|
remove_tmps = True
|
|
finally:
|
|
if GS.debug_enabled and not remove_tmps:
|
|
if self._files_to_remove:
|
|
logger.warning(W_KEEPTMP+'Keeping temporal files: '+str(self._files_to_remove))
|
|
else:
|
|
self.remove_temporals()
|
|
|
|
def get_targets(self, out_dir):
|
|
pcb_name = self._parent.expand_filename(out_dir, self.output)
|
|
files = [pcb_name]
|
|
if self.create_preview:
|
|
files.append(os.path.splitext(pcb_name)[0]+'.png')
|
|
return files
|
|
|
|
|
|
@output_class
|
|
class Panelize(BaseOutput): # noqa: F821
|
|
""" Panelize
|
|
Creates a panel to fabricate various copies of the PCB at once.
|
|
It currently uses the KiKit tool, which must be available.
|
|
Consult KiKit docs for detailed information.
|
|
[KiKit panelization docs](https://github.com/yaqwsx/KiKit/blob/master/doc/examples.md).
|
|
Current versions of KiKit only support KiCad 6 and my tests using
|
|
KiKit 1.0.5 (the last to support KiCad 5) shown some
|
|
incompatibilities.
|
|
Note that you don't need to specify the units for all distances.
|
|
If they are omitted they are assumed to be `units`.
|
|
The same is valid for angles, using `default_angles` """
|
|
def __init__(self):
|
|
super().__init__()
|
|
with document:
|
|
self.options = PanelizeOptions
|
|
""" *[dict] Options for the `Panelize` output """
|
|
self._category = 'PCB/fabrication'
|
|
|
|
@staticmethod
|
|
def get_conf_examples(name, layers):
|
|
if not GS.check_tool(name, 'KiKit'):
|
|
return None
|
|
outs = []
|
|
register_xmp_import('PanelDemo_4x4')
|
|
return outs
|