Added internal plot of the worksheet to pcb_print
- Also renamed some options to make it simpler (sorry)
This commit is contained in:
parent
9c2f8ccd58
commit
4b341a7a92
2
Makefile
2
Makefile
|
|
@ -71,7 +71,7 @@ test_docker_local_1:
|
|||
# Also change the owner of the files to the current user (we run as root like in GitHub)
|
||||
#docker run --rm -it -v $(CWD):$(CWD) --workdir="$(CWD)" setsoft/kicad_auto_test:latest '/bin/bash'
|
||||
docker run --rm -v $(CWD):$(CWD) --workdir="$(CWD)" setsoft/kicad_auto_test:latest \
|
||||
/bin/bash -c "flake8 . --count --statistics ; python3-coverage run -a src/kibot --help-outputs > /dev/null; pytest-3 --log-cli-level debug -k 'test_print_sch_variant_ni_1' --test_dir output ; $(PY_COV) html; chown -R $(USER_ID):$(GROUP_ID) output/ tests/board_samples/ tests/.config/kiplot/plugins/__pycache__/ tests/test_plot/fake_pcbnew/__pycache__/ tests/.config/kibot/plugins/__pycache__/ .coverage htmlcov/"
|
||||
/bin/bash -c "flake8 . --count --statistics ; python3-coverage run -a src/kibot --help-outputs > /dev/null; pytest-3 --log-cli-level debug -k 'test_report_simple_2' --test_dir output ; $(PY_COV) html; chown -R $(USER_ID):$(GROUP_ID) output/ tests/board_samples/ tests/.config/kiplot/plugins/__pycache__/ tests/test_plot/fake_pcbnew/__pycache__/ tests/.config/kibot/plugins/__pycache__/ .coverage htmlcov/"
|
||||
#$(PY_COV) report
|
||||
#x-www-browser htmlcov/index.html
|
||||
rm .coverage
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -1535,6 +1535,7 @@ Next time you need this list just use an alias, like this:
|
|||
- `name`: [string=''] Used to identify this particular output definition.
|
||||
- `options`: [dict] Options for the `pcb_print` output.
|
||||
* Valid keys:
|
||||
- `blind_via_color`: [string=''] Color used for blind/buried `colored_vias`.
|
||||
- `color_theme`: [string='_builtin_classic'] Selects the color theme. Only applies to KiCad 6.
|
||||
To use the KiCad 6 default colors select `_builtin_default`.
|
||||
Usually user colors are stored as `user`, but you can give it another name.
|
||||
|
|
@ -1543,12 +1544,19 @@ Next time you need this list just use an alias, like this:
|
|||
- `dnf_filter`: [string|list(string)='_none'] Name of the filter to mark components as not fitted.
|
||||
A short-cut to use for simple cases where a variant is an overkill.
|
||||
- `drill_marks`: [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale).
|
||||
- `enable_ki6_frame_fix`: [boolean=false] KiCad 6 doesn't support custom title-block/frames from Python.
|
||||
This option uses KiCad GUI to print the frame, is slow, but works.
|
||||
Always enabled for KiCad 5, which crashes if we try to plot the frame.
|
||||
- `format`: [string='PDF'] [PDF,SVG,PNG,EPS,PS] Format for the output file/s.
|
||||
Note that for PS you need `ghostscript` which isn't part of the default docker images.
|
||||
- `frame_plot_mechanism`: [string='internal'] [gui,internal,plot] Plotting the frame from Python is problematic.
|
||||
This option selects a workaround strategy.
|
||||
gui: uses KiCad GUI to do it. Is slow but you get the correct frame.
|
||||
But it can't keep track of page numbers.
|
||||
internal: KiBot loads the `.kicad_wks` and does the drawing work.
|
||||
Best option, but some details are different from what the GUI generates.
|
||||
plot: uses KiCad Python API. Only available for KiCad 6.
|
||||
You get the default frame and some substitutions doesn't work.
|
||||
- `hide_excluded`: [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant.
|
||||
- `keep_temporal_files`: [boolean=false] Store the temporal page and layer files in the output dir and don't delete them.
|
||||
- `micro_via_color`: [string=''] Color used for micro `colored_vias`.
|
||||
- `output`: [string='%f-%i%I%v.%x'] Filename for the output (%i=assembly, %x=pdf)/(%i=assembly_page_NN, %x=svg). Affected by global options.
|
||||
- *output_name*: Alias for output.
|
||||
- `pad_color`: [string=''] Color used for `colored_pads`.
|
||||
|
|
@ -1583,7 +1591,7 @@ Next time you need this list just use an alias, like this:
|
|||
- `title`: [string=''] Text used to replace the sheet title. %VALUE expansions are allowed.
|
||||
If it starts with `+` the text is concatenated.
|
||||
- `variant`: [string=''] Board variant to apply.
|
||||
- `via_color`: [string=''] Color used for `colored_vias`.
|
||||
- `via_color`: [string=''] Color used for through-hole `colored_vias`.
|
||||
- `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output.
|
||||
- `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested.
|
||||
|
||||
|
|
|
|||
|
|
@ -960,6 +960,8 @@ outputs:
|
|||
type: 'pcb_print'
|
||||
dir: 'Example/pcb_print_dir'
|
||||
options:
|
||||
# [string=''] Color used for blind/buried `colored_vias`
|
||||
blind_via_color: ''
|
||||
# [string='_builtin_classic'] Selects the color theme. Only applies to KiCad 6.
|
||||
# To use the KiCad 6 default colors select `_builtin_default`.
|
||||
# Usually user colors are stored as `user`, but you can give it another name
|
||||
|
|
@ -973,15 +975,24 @@ outputs:
|
|||
dnf_filter: '_none'
|
||||
# [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale)
|
||||
drill_marks: 'full'
|
||||
# [boolean=false] KiCad 6 doesn't support custom title-block/frames from Python.
|
||||
# This option uses KiCad GUI to print the frame, is slow, but works.
|
||||
# Always enabled for KiCad 5, which crashes if we try to plot the frame
|
||||
enable_ki6_frame_fix: false
|
||||
# [string='PDF'] [PDF,SVG,PNG,EPS,PS] Format for the output file/s.
|
||||
# Note that for PS you need `ghostscript` which isn't part of the default docker images
|
||||
format: 'PDF'
|
||||
# [string='internal'] [gui,internal,plot] Plotting the frame from Python is problematic.
|
||||
# This option selects a workaround strategy.
|
||||
# gui: uses KiCad GUI to do it. Is slow but you get the correct frame.
|
||||
# But it can't keep track of page numbers.
|
||||
# internal: KiBot loads the `.kicad_wks` and does the drawing work.
|
||||
# Best option, but some details are different from what the GUI generates.
|
||||
# plot: uses KiCad Python API. Only available for KiCad 6.
|
||||
# You get the default frame and some substitutions doesn't work
|
||||
frame_plot_mechanism: 'internal'
|
||||
# [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant
|
||||
hide_excluded: false
|
||||
# [boolean=false] Store the temporal page and layer files in the output dir and don't delete them
|
||||
keep_temporal_files: false
|
||||
# [string=''] Color used for micro `colored_vias`
|
||||
micro_via_color: ''
|
||||
# [string='%f-%i%I%v.%x'] Filename for the output (%i=assembly, %x=pdf)/(%i=assembly_page_NN, %x=svg). Affected by global options
|
||||
output: '%f-%i%I%v.%x'
|
||||
# `output_name` is an alias for `output`
|
||||
|
|
@ -1042,7 +1053,7 @@ outputs:
|
|||
title: ''
|
||||
# [string=''] Board variant to apply
|
||||
variant: ''
|
||||
# [string=''] Color used for `colored_vias`
|
||||
# [string=''] Color used for through-hole `colored_vias`
|
||||
via_color: ''
|
||||
# PcbDraw - Beautiful 2D PCB render:
|
||||
# Uses configurable colors.
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ class GS(object):
|
|||
return GS.kicad_version_n < KICAD_VERSION_5_99
|
||||
|
||||
@staticmethod
|
||||
def expand_text_variables(text):
|
||||
def expand_text_variables(text, extra_vars=None):
|
||||
vars = GS.load_pro_variables()
|
||||
new_text = ''
|
||||
last = 0
|
||||
|
|
@ -276,6 +276,8 @@ class GS(object):
|
|||
for match in GS.vars_regex.finditer(text):
|
||||
vname = match.group(1)
|
||||
value = vars.get(vname, None)
|
||||
if value is None and extra_vars is not None:
|
||||
value = extra_vars.get(vname, None)
|
||||
if value is None:
|
||||
value = '${'+vname+'}'
|
||||
logger.warning(W_UNKVAR+"Unknown text variable `{}`".format(vname))
|
||||
|
|
|
|||
|
|
@ -395,34 +395,56 @@ class KiConf(object):
|
|||
logger.debug('Copying {} -> {}'.format(fname, dest))
|
||||
copy2(fname, dest)
|
||||
data[key]['page_layout_descr_file'] = dest
|
||||
return dest
|
||||
else:
|
||||
logger.error('Missing page layout file: '+fname)
|
||||
exit(MISSING_WKS)
|
||||
return None
|
||||
|
||||
def fix_page_layout_k6(project):
|
||||
def fix_page_layout_k6(project, dry):
|
||||
# Get the current definitions
|
||||
dest_dir = os.path.dirname(project)
|
||||
with open(project, 'rt') as f:
|
||||
pro_text = f.read()
|
||||
data = json.loads(pro_text)
|
||||
KiConf.fix_page_layout_k6_key('pcbnew', data, dest_dir)
|
||||
KiConf.fix_page_layout_k6_key('schematic', data, dest_dir)
|
||||
with open(project, 'wt') as f:
|
||||
f.write(json.dumps(data, sort_keys=True, indent=2))
|
||||
layouts = [None, None]
|
||||
if not dry:
|
||||
layouts[1] = KiConf.fix_page_layout_k6_key('pcbnew', data, dest_dir)
|
||||
layouts[0] = KiConf.fix_page_layout_k6_key('schematic', data, dest_dir)
|
||||
with open(project, 'wt') as f:
|
||||
f.write(json.dumps(data, sort_keys=True, indent=2))
|
||||
else:
|
||||
aux = data.get('schematic', None)
|
||||
if aux:
|
||||
layouts[0] = KiConf.expand_env(aux.get('page_layout_descr_file', None))
|
||||
aux = data.get('pcbnew', None)
|
||||
if aux:
|
||||
layouts[1] = KiConf.expand_env(aux.get('page_layout_descr_file', None))
|
||||
return layouts
|
||||
|
||||
def fix_page_layout_k5(project):
|
||||
def fix_page_layout_k5(project, dry):
|
||||
order = 1
|
||||
dest_dir = os.path.dirname(project)
|
||||
with open(project, 'rt') as f:
|
||||
lns = f.readlines()
|
||||
is_pcb_new = False
|
||||
layouts = [None, None]
|
||||
for c, line in enumerate(lns):
|
||||
if line.startswith('[pcbnew]'):
|
||||
is_pcb_new = True
|
||||
if line.startswith('[schematic'):
|
||||
is_pcb_new = False
|
||||
if line.startswith('PageLayoutDescrFile='):
|
||||
fname = line[20:].strip()
|
||||
if fname:
|
||||
fname = KiConf.expand_env(fname)
|
||||
if os.path.isfile(fname):
|
||||
dest = os.path.join(dest_dir, str(order)+'.kicad_wks')
|
||||
copy2(fname, dest)
|
||||
if not dry:
|
||||
copy2(fname, dest)
|
||||
layouts[is_pcb_new] = dest
|
||||
else:
|
||||
layouts[is_pcb_new] = fname
|
||||
order = order+1
|
||||
else:
|
||||
logger.error('Missing page layout file: '+fname)
|
||||
|
|
@ -430,17 +452,18 @@ class KiConf(object):
|
|||
else:
|
||||
dest = ''
|
||||
lns[c] = 'PageLayoutDescrFile='+dest+'\n'
|
||||
with open(project, 'wt') as f:
|
||||
lns = f.writelines(lns)
|
||||
if not dry:
|
||||
with open(project, 'wt') as f:
|
||||
lns = f.writelines(lns)
|
||||
return layouts
|
||||
|
||||
def fix_page_layout(project):
|
||||
def fix_page_layout(project, dry=False):
|
||||
if not project:
|
||||
return
|
||||
return None, None
|
||||
KiConf.init(GS.pcb_file)
|
||||
if GS.ki5():
|
||||
KiConf.fix_page_layout_k5(project)
|
||||
else:
|
||||
KiConf.fix_page_layout_k6(project)
|
||||
return KiConf.fix_page_layout_k5(project, dry)
|
||||
return KiConf.fix_page_layout_k6(project, dry)
|
||||
|
||||
def expand_env(name, used_extra=None):
|
||||
if used_extra is None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Salvador E. Tropea
|
||||
# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
"""
|
||||
KiCad v5/6 PCB format.
|
||||
Currently used only for the paper size
|
||||
"""
|
||||
from .sexpdata import load, SExpData
|
||||
from .v6_sch import _check_str, _check_symbol, _check_is_symbol_list, _check_float
|
||||
PAGE_SIZE = {'A0': (841, 1189),
|
||||
'A1': (594, 841),
|
||||
'A2': (420, 594),
|
||||
'A3': (297, 420),
|
||||
'A4': (210, 297),
|
||||
'A5': (148, 210),
|
||||
'A': (215.9, 279.4),
|
||||
'B': (279.4, 431.8),
|
||||
'C': (431.8, 558.8),
|
||||
'D': (558.8, 863.6),
|
||||
'E': (863.6, 1117.6),
|
||||
'USLetter': (215.9, 279.4),
|
||||
'USLegal': (215.9, 355.6),
|
||||
'USLedger': (279.4, 431.8)}
|
||||
|
||||
|
||||
class PCBError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PCB(object):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.paper = 'A4'
|
||||
self.paper_portrait = False
|
||||
self.paper_w = self.paper_h = 0
|
||||
|
||||
@staticmethod
|
||||
def load(file):
|
||||
with open(file, 'rt') as fh:
|
||||
error = None
|
||||
try:
|
||||
pcb = load(fh)[0]
|
||||
except SExpData as e:
|
||||
error = str(e)
|
||||
if error:
|
||||
raise PCBError(error)
|
||||
if not isinstance(pcb, list) or pcb[0].value() != 'kicad_pcb':
|
||||
raise PCBError('No kicad_pcb signature')
|
||||
o = PCB()
|
||||
for e in pcb[1:]:
|
||||
e_type = _check_is_symbol_list(e)
|
||||
if e_type == 'paper' or e_type == 'page':
|
||||
o.paper = _check_str(e, 1, e_type) if e_type == 'paper' else _check_symbol(e, 1, e_type)
|
||||
if o.paper == 'User':
|
||||
o.paper_w = _check_float(e, 2, e_type)
|
||||
o.paper_h = _check_float(e, 3, e_type)
|
||||
else:
|
||||
if o.paper not in PAGE_SIZE:
|
||||
raise PCBError('Unknown paper size selected {}'.format(o.paper))
|
||||
size = PAGE_SIZE[o.paper]
|
||||
if len(e) > 2 and _check_symbol(e, 2, e_type) == 'portrait':
|
||||
o.paper_portrait = True
|
||||
o.paper_w = size[0]
|
||||
o.paper_h = size[1]
|
||||
else:
|
||||
o.paper_w = size[1]
|
||||
o.paper_h = size[0]
|
||||
break
|
||||
return o
|
||||
|
|
@ -85,6 +85,17 @@ def _check_str(items, pos, name):
|
|||
return value
|
||||
|
||||
|
||||
def _check_relaxed(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, Symbol):
|
||||
return value.value()
|
||||
if isinstance(value, (float, int)):
|
||||
return str(value)
|
||||
raise SchError('{} is not a string, Symbol or number `{}`'.format(name, value))
|
||||
|
||||
|
||||
def _check_symbol_value(items, pos, name, sym):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, list) or not isinstance(value[0], Symbol) or value[0].value() != sym:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,511 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022 Salvador E. Tropea
|
||||
# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
# KiCad bugs:
|
||||
# - Text bold doesn't work
|
||||
# - Shape Line and Rect swapped
|
||||
"""
|
||||
KiCad v5/6 Worksheet format.
|
||||
A basic implementation of the .kicad_wks file format.
|
||||
Documentation: https://dev-docs.kicad.org/en/file-formats/sexpr-worksheet/
|
||||
"""
|
||||
import io
|
||||
from struct import unpack
|
||||
from pcbnew import (wxPoint, wxSize, FromMM, GR_TEXT_HJUSTIFY_LEFT, GR_TEXT_HJUSTIFY_RIGHT, GR_TEXT_HJUSTIFY_CENTER,
|
||||
GR_TEXT_VJUSTIFY_TOP, GR_TEXT_VJUSTIFY_CENTER, GR_TEXT_VJUSTIFY_BOTTOM, FILL_T_FILLED_SHAPE,
|
||||
SHAPE_T_POLY, wxPointMM)
|
||||
import pcbnew
|
||||
from .sexpdata import load, SExpData
|
||||
from .v6_sch import (_check_is_symbol_list, _check_float, _check_integer, _check_symbol_value, _check_str, _check_symbol,
|
||||
_check_relaxed, _get_points, _check_symbol_str)
|
||||
from ..svgutils.transform import ImageElement, GroupElement
|
||||
from ..misc import W_WKSVERSION
|
||||
from ..gs import GS
|
||||
from .. import log
|
||||
|
||||
logger = log.get_logger()
|
||||
setup = None
|
||||
SUP_VERSION = 20210606
|
||||
|
||||
# TODO
|
||||
# - Mover los draw() a cada clase
|
||||
KI5_2_KI6 = {'K': 'KICAD_VERSION', 'S': '#', 'N': '##', 'C0': 'COMMENT1', 'C1': 'COMMENT2', 'C2': 'COMMENT3',
|
||||
'C3': 'COMMENT4', 'C4': 'COMMENT5', 'C5': 'COMMENT6', 'C6': 'COMMENT7', 'C7': 'COMMENT8',
|
||||
'C8': 'COMMENT9', 'Y': 'COMPANY', 'F': 'FILENAME', 'D': 'ISSUE_DATE', 'Z': 'PAPER', 'R': 'REVISION',
|
||||
'P': 'SHEETNAME', 'T': 'TITLE'}
|
||||
|
||||
|
||||
class WksError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def text_from_ki5(text):
|
||||
for k, v in KI5_2_KI6.items():
|
||||
text = text.replace('%'+k, '${'+v+'}')
|
||||
text = text.replace('%%', '%')
|
||||
return text
|
||||
|
||||
|
||||
def _check_mm(items, pos, name):
|
||||
return FromMM(_check_float(items, pos, name))
|
||||
|
||||
|
||||
def _get_point(items, pos, sname, name):
|
||||
value = _check_symbol_value(items, pos, name, sname)
|
||||
ref = 'rbcorner'
|
||||
if len(value) > 3:
|
||||
ref = _check_symbol(value, 3, sname+' reference')
|
||||
return wxPoint(_check_mm(value, 1, sname+' x'), _check_mm(value, 2, sname+' y')), ref
|
||||
|
||||
|
||||
def _get_size(items, pos, sname, name):
|
||||
value = _check_symbol_value(items, pos, name, sname)
|
||||
return wxSize(_check_mm(value, 1, sname+' x'), _check_mm(value, 2, sname+' y'))
|
||||
|
||||
|
||||
class WksSetup(object):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.text_w = self.text_h = FromMM(1.5)
|
||||
self.line_width = self.text_line_width = FromMM(0.15)
|
||||
self.left_margin = self.right_margin = self.top_margin = self.bottom_margin = FromMM(10)
|
||||
|
||||
@staticmethod
|
||||
def parse(items):
|
||||
s = WksSetup()
|
||||
for i in items[1:]:
|
||||
i_type = _check_is_symbol_list(i)
|
||||
if i_type == 'textsize':
|
||||
s.text_w = _check_mm(i, 1, 'textsize width')
|
||||
s.text_h = _check_mm(i, 2, 'textsize height')
|
||||
elif i_type == 'linewidth':
|
||||
s.line_width = _check_mm(i, 1, i_type)
|
||||
elif i_type == 'textlinewidth':
|
||||
s.text_line_width = _check_mm(i, 1, i_type)
|
||||
elif i_type in ['left_margin', 'right_margin', 'top_margin', 'bottom_margin']:
|
||||
setattr(s, i_type, _check_mm(i, 1, i_type))
|
||||
else:
|
||||
raise WksError('Unknown setup attribute `{}`'.format(i))
|
||||
return s
|
||||
|
||||
|
||||
class WksDrawing(object):
|
||||
c_name = 'base'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.repeat = 1
|
||||
self.incrx = self.incry = 0
|
||||
self.comment = ''
|
||||
self.name = ''
|
||||
self.option = ''
|
||||
|
||||
def parse_fixed_args(self, items):
|
||||
""" Default parser for fixed arguments.
|
||||
Used when no fixed args are used. """
|
||||
return 1
|
||||
|
||||
def parse_specific_args(self, i_type, i, items, offset):
|
||||
""" Default parser for arguments specific for the class. """
|
||||
raise WksError('Unknown {} attribute `{}`'.format(self.c_name, i))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, items):
|
||||
s = cls()
|
||||
offset = s.parse_fixed_args(items)
|
||||
for c, i in enumerate(items[offset:]):
|
||||
i_type = _check_is_symbol_list(i)
|
||||
if i_type == 'repeat':
|
||||
s.repeat = _check_integer(i, 1, i_type)
|
||||
elif i_type in ['incrx', 'incry']:
|
||||
setattr(s, i_type, _check_mm(i, 1, i_type))
|
||||
elif i_type == 'comment':
|
||||
s.comment = _check_str(i, 1, i_type)
|
||||
elif i_type == 'name':
|
||||
s.nm = _check_relaxed(i, 1, i_type)
|
||||
elif i_type == 'option':
|
||||
# Not documented 2022/04/15
|
||||
s.option = _check_symbol(i, 1, i_type)
|
||||
else:
|
||||
s.parse_specific_args(i_type, i, items, c+offset)
|
||||
return s
|
||||
|
||||
|
||||
class WksLine(WksDrawing):
|
||||
c_name = 'line'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.start = wxPoint(0, 0)
|
||||
self.start_ref = 'rbcorner'
|
||||
self.end = wxPoint(0, 0)
|
||||
self.end_ref = 'rbcorner'
|
||||
self.line_width = setup.line_width
|
||||
|
||||
def parse_specific_args(self, i_type, i, items, offset):
|
||||
if i_type == 'linewidth':
|
||||
self.line_width = _check_mm(i, 1, i_type)
|
||||
elif i_type == 'start':
|
||||
self.start, self.start_ref = _get_point(items, offset, i_type, self.c_name)
|
||||
elif i_type == 'end':
|
||||
self.end, self.end_ref = _get_point(items, offset, i_type, self.c_name)
|
||||
else:
|
||||
super().parse_specific_args(i_type, i, items, offset)
|
||||
|
||||
|
||||
class WksRect(WksLine):
|
||||
c_name = 'rect'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class WksFont(object):
|
||||
name = 'font'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.bold = False
|
||||
self.italic = False
|
||||
self.size = wxSize(setup.text_w, setup.text_h)
|
||||
self.line_width = setup.text_line_width
|
||||
|
||||
@staticmethod
|
||||
def parse(items):
|
||||
s = WksFont()
|
||||
for c, i in enumerate(items[1:]):
|
||||
i_type = _check_is_symbol_list(i, allow_orphan_symbol=('bold', 'italic'))
|
||||
if i_type == 'bold':
|
||||
s.bold = True
|
||||
elif i_type == 'italic':
|
||||
s.italic = True
|
||||
elif i_type == 'size':
|
||||
s.size = _get_size(items, c+1, i_type, WksFont.name)
|
||||
elif i_type == 'linewidth':
|
||||
s.line_width = _check_mm(i, 1, i_type)
|
||||
else:
|
||||
raise WksError('Unknown font attribute `{}`'.format(i))
|
||||
return s
|
||||
|
||||
|
||||
class WksText(WksDrawing):
|
||||
c_name = 'tbtext'
|
||||
V_JUSTIFY = {'top': GR_TEXT_VJUSTIFY_TOP, 'bottom': GR_TEXT_VJUSTIFY_BOTTOM}
|
||||
H_JUSTIFY = {'center': GR_TEXT_HJUSTIFY_CENTER, 'right': GR_TEXT_HJUSTIFY_RIGHT, 'left': GR_TEXT_HJUSTIFY_LEFT}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pos = wxPoint(0, 0)
|
||||
self.pos_ref = 'rbcorner'
|
||||
self.font = WksFont()
|
||||
self.h_justify = GR_TEXT_HJUSTIFY_LEFT
|
||||
self.v_justify = GR_TEXT_VJUSTIFY_CENTER
|
||||
self.text = ''
|
||||
self.rotate = 0
|
||||
self.max_len = 0
|
||||
self.max_height = 0
|
||||
self.incr_label = 1
|
||||
|
||||
def parse_fixed_args(self, items):
|
||||
self.text = _check_relaxed(items, 1, self.c_name+' text')
|
||||
return 2
|
||||
|
||||
def parse_specific_args(self, i_type, i, items, offset):
|
||||
if i_type == 'rotate':
|
||||
self.rotate = _check_float(i, 1, i_type)
|
||||
elif i_type == 'pos':
|
||||
self.pos, self.pos_ref = _get_point(items, offset, i_type, self.c_name)
|
||||
elif i_type == 'justify':
|
||||
# Not documented 2022/04/15
|
||||
for index in range(len(i)-1):
|
||||
val = _check_symbol(i, index+1, i_type)
|
||||
if val in WksText.V_JUSTIFY:
|
||||
self.v_justify = WksText.V_JUSTIFY[val]
|
||||
elif val in WksText.H_JUSTIFY:
|
||||
self.h_justify = WksText.H_JUSTIFY[val]
|
||||
else:
|
||||
raise WksError('Unknown justify value `{}`'.format(val))
|
||||
elif i_type == 'font':
|
||||
self.font = WksFont.parse(i)
|
||||
elif i_type == 'maxlen':
|
||||
# Not documented 2022/04/15
|
||||
self.max_len = _check_mm(i, 1, i_type)
|
||||
elif i_type == 'maxheight':
|
||||
# Not documented 2022/04/15
|
||||
self.max_height = _check_mm(i, 1, i_type)
|
||||
elif i_type == 'incrlabel':
|
||||
# Not documented 2022/04/15
|
||||
self.incr_label = _check_integer(i, 1, i_type)
|
||||
else:
|
||||
super().parse_specific_args(i_type, i, items, offset)
|
||||
|
||||
|
||||
class WksPolygon(WksDrawing):
|
||||
c_name = 'polygon'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pos = wxPoint(0, 0)
|
||||
self.pos_ref = 'rbcorner'
|
||||
self.rotate = 0
|
||||
self.line_width = setup.line_width
|
||||
self.pts = []
|
||||
|
||||
def parse_specific_args(self, i_type, i, items, offset):
|
||||
if i_type == 'rotate':
|
||||
self.rotate = _check_float(i, 1, i_type)
|
||||
elif i_type == 'pos':
|
||||
self.pos, self.pos_ref = _get_point(items, offset, i_type, self.c_name)
|
||||
elif i_type == 'linewidth':
|
||||
self.line_width = _check_mm(i, 1, i_type)
|
||||
elif i_type == 'pts':
|
||||
self.pts.append([wxPointMM(p.x, p.y) for p in _get_points(i)])
|
||||
else:
|
||||
super().parse_specific_args(i_type, i, items, offset)
|
||||
|
||||
|
||||
class WksBitmap(WksDrawing):
|
||||
c_name = 'bitmap'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pos = wxPoint(0, 0)
|
||||
self.pos_ref = 'rbcorner'
|
||||
self.scale = 1.0
|
||||
self.data = b''
|
||||
|
||||
def parse_specific_args(self, i_type, i, items, offset):
|
||||
if i_type == 'pos':
|
||||
self.pos, self.pos_ref = _get_point(items, offset, i_type, self.c_name)
|
||||
elif i_type == 'scale':
|
||||
self.scale = _check_float(i, 1, i_type)
|
||||
elif i_type == 'pngdata':
|
||||
for c in range(len(i)-1):
|
||||
v = _check_symbol_str(i, c+1, self.c_name+' pngdata', 'data')
|
||||
self.data += bytes([int(c, 16) for c in v.split(' ') if c])
|
||||
else:
|
||||
super().parse_specific_args(i_type, i, items, offset)
|
||||
|
||||
|
||||
class Worksheet(object):
|
||||
def __init__(self, setup, elements, version, generator, has_images):
|
||||
super().__init__()
|
||||
self.setup = setup
|
||||
self.elements = elements
|
||||
self.version = version
|
||||
self.generator = generator
|
||||
self.has_images = has_images
|
||||
|
||||
@staticmethod
|
||||
def load(file):
|
||||
with open(file, 'rt') as fh:
|
||||
error = None
|
||||
try:
|
||||
wks = load(fh)[0]
|
||||
except SExpData as e:
|
||||
error = str(e)
|
||||
if error:
|
||||
raise WksError(error)
|
||||
if not isinstance(wks, list) or (wks[0].value() != 'page_layout' and wks[0].value() != 'kicad_wks'):
|
||||
raise WksError('No kicad_wks signature')
|
||||
elements = []
|
||||
global setup
|
||||
setup = WksSetup()
|
||||
version = 0
|
||||
generator = ''
|
||||
has_images = False
|
||||
for e in wks[1:]:
|
||||
e_type = _check_is_symbol_list(e)
|
||||
if e_type == 'setup':
|
||||
setup = WksSetup.parse(e)
|
||||
elif e_type == 'rect':
|
||||
elements.append(WksRect.parse(e))
|
||||
elif e_type == 'line':
|
||||
elements.append(WksLine.parse(e))
|
||||
elif e_type == 'tbtext':
|
||||
obj = WksText.parse(e)
|
||||
if not version:
|
||||
obj.text = text_from_ki5(obj.text)
|
||||
elements.append(obj)
|
||||
elif e_type == 'polygon':
|
||||
elements.append(WksPolygon.parse(e))
|
||||
elif e_type == 'bitmap':
|
||||
elements.append(WksBitmap.parse(e))
|
||||
has_images = True
|
||||
elif e_type == 'version':
|
||||
version = _check_integer(e, 1, e_type)
|
||||
if version > SUP_VERSION:
|
||||
logger.warning(W_WKSVERSION+"Unsupported worksheet version, loading could fail")
|
||||
elif e_type == 'generator':
|
||||
generator = _check_symbol(e, 1, e_type)
|
||||
else:
|
||||
raise WksError('Unknown worksheet attribute `{}`'.format(e_type))
|
||||
return Worksheet(setup, elements, version, generator, has_images)
|
||||
|
||||
def set_page(self, pw, ph):
|
||||
pw = FromMM(pw)
|
||||
ph = FromMM(ph)
|
||||
self.pw = pw
|
||||
self.ph = ph
|
||||
self.lm = self.setup.left_margin
|
||||
self.tm = self.setup.top_margin
|
||||
self.rm = pw-self.setup.right_margin
|
||||
self.bm = ph-self.setup.bottom_margin
|
||||
|
||||
def solve_ref(self, pt, inc_x, inc_y, ref):
|
||||
pt = wxPoint(pt.x, pt.y) # Make a copy
|
||||
if ref[0] == 'l':
|
||||
pt.x += self.lm
|
||||
elif ref[0] == 'r':
|
||||
pt.x = self.rm-pt.x
|
||||
inc_x = -inc_x
|
||||
if ref[1] == 't':
|
||||
pt.y += self.tm
|
||||
elif ref[1] == 'b':
|
||||
pt.y = self.bm-pt.y
|
||||
inc_y = -inc_y
|
||||
return pt, wxPoint(inc_x, inc_y)
|
||||
|
||||
def check_page(self, e):
|
||||
return e.option and ((e.option == 'page1only' and self.page != 1) or (e.option == 'notonpage1' and self.page == 1))
|
||||
|
||||
def draw_start_end(self, e, shape):
|
||||
st, sti = self.solve_ref(e.start, e.incrx, e.incry, e.start_ref)
|
||||
en, eni = self.solve_ref(e.end, e.incrx, e.incry, e.end_ref)
|
||||
for _ in range(e.repeat):
|
||||
s = pcbnew.PCB_SHAPE()
|
||||
s.SetShape(shape)
|
||||
s.SetStart(st)
|
||||
s.SetEnd(en)
|
||||
s.SetWidth(e.line_width)
|
||||
s.SetLayer(self.layer)
|
||||
self.board.Add(s)
|
||||
self.pcb_items.append(s)
|
||||
st += sti
|
||||
en += eni
|
||||
if st.x > self.rm or st.y > self.bm:
|
||||
break
|
||||
|
||||
def draw_line(self, e):
|
||||
self.draw_start_end(e, 0)
|
||||
|
||||
def draw_rect(self, e):
|
||||
self.draw_start_end(e, 1)
|
||||
|
||||
def draw_text(self, e):
|
||||
pos, posi = self.solve_ref(e.pos, e.incrx, e.incry, e.pos_ref)
|
||||
text = GS.expand_text_variables(e.text, self.tb_vars)
|
||||
for _ in range(e.repeat):
|
||||
s = pcbnew.PCB_TEXT(None)
|
||||
s.SetText(text)
|
||||
s.SetPosition(pos)
|
||||
s.SetTextSize(e.font.size)
|
||||
if e.font.bold:
|
||||
s.SetBold(True)
|
||||
s.SetTextThickness(round(e.font.line_width*2))
|
||||
else:
|
||||
s.SetTextThickness(e.font.line_width)
|
||||
s.SetHorizJustify(e.h_justify)
|
||||
s.SetVertJustify(e.v_justify)
|
||||
s.SetLayer(self.layer)
|
||||
if e.font.italic:
|
||||
s.SetItalic(True)
|
||||
if e.rotate:
|
||||
s.SetTextAngle(e.rotate*10)
|
||||
# Adjust the text size to the maximum allowed
|
||||
if e.max_len > 0:
|
||||
w = s.GetBoundingBox().GetWidth()
|
||||
if w > e.max_len:
|
||||
s.SetTextWidth(round(e.font.size.x*e.max_len/w))
|
||||
if e.max_height > 0:
|
||||
h = s.GetBoundingBox().GetHeight()
|
||||
if h > e.max_height:
|
||||
s.SetTextHeight(round(e.font.size.y*e.max_height/h))
|
||||
# Add it to the board and to the list of things to remove
|
||||
self.board.Add(s)
|
||||
self.pcb_items.append(s)
|
||||
# Increment the position
|
||||
pos += posi
|
||||
if pos.x > self.rm or pos.y > self.bm:
|
||||
break
|
||||
# Increment the text
|
||||
# This is what KiCad does ... not very cleaver
|
||||
if text:
|
||||
text_end = text[-1]
|
||||
if text_end.isdigit():
|
||||
# Only increment the last digit "9" -> "10", "10" -> "11", "19" -> "110"?!!
|
||||
text_end = str(int(text_end)+e.incr_label)
|
||||
else:
|
||||
text_end = chr((ord(text_end)+e.incr_label) % 256)
|
||||
text = text[:-1]+text_end
|
||||
else:
|
||||
text = '?'
|
||||
|
||||
def draw_polygon(self, e):
|
||||
pos, posi = self.solve_ref(e.pos, e.incrx, e.incry, e.pos_ref)
|
||||
for _ in range(e.repeat):
|
||||
for pts in e.pts:
|
||||
s = pcbnew.PCB_SHAPE()
|
||||
s.SetShape(SHAPE_T_POLY)
|
||||
s.SetFillMode(FILL_T_FILLED_SHAPE)
|
||||
s.SetPolyPoints([pos+p for p in pts])
|
||||
s.SetWidth(e.line_width)
|
||||
s.SetLayer(self.layer)
|
||||
if e.rotate:
|
||||
s.Rotate(pos, e.rotate*10)
|
||||
self.board.Add(s)
|
||||
self.pcb_items.append(s)
|
||||
pos += posi
|
||||
|
||||
def draw(self, board, layer, page, page_w, page_h, tb_vars):
|
||||
self.pcb_items = []
|
||||
self.set_page(page_w, page_h)
|
||||
self.layer = layer
|
||||
self.board = board
|
||||
self.page = page
|
||||
self.tb_vars = tb_vars
|
||||
self.images = []
|
||||
for e in self.elements:
|
||||
if self.check_page(e):
|
||||
continue
|
||||
if e.c_name == 'line':
|
||||
self.draw_line(e)
|
||||
elif e.c_name == 'rect':
|
||||
self.draw_rect(e)
|
||||
elif e.c_name == 'tbtext':
|
||||
self.draw_text(e)
|
||||
elif e.c_name == 'polygon':
|
||||
self.draw_polygon(e)
|
||||
elif e.c_name == 'bitmap':
|
||||
# Can we draw it using KiCad? I don't see how
|
||||
# Make a list to be added to the SVG output
|
||||
self.images.append(e)
|
||||
|
||||
def add_images_to_svg(self, svg):
|
||||
for e in self.images:
|
||||
s = e.data
|
||||
w, h = unpack('>LL', s[16:24])
|
||||
# For KiCad 300 dpi is 1:1 scale
|
||||
dpi = 300/e.scale
|
||||
# Convert pixels to mm and then to KiCad units
|
||||
w = FromMM(w/dpi*25.4)
|
||||
h = FromMM(h/dpi*25.4)
|
||||
# KiCad informs the position for the center of the image
|
||||
pos, posi = self.solve_ref(e.pos, e.incrx, e.incry, e.pos_ref)
|
||||
for _ in range(e.repeat):
|
||||
img = ImageElement(io.BytesIO(s), w, h)
|
||||
img.moveto(pos.x-round(w/2), pos.y-round(h/2))
|
||||
# Put the image in a group
|
||||
g = GroupElement([img])
|
||||
# Add the group to the SVG
|
||||
svg.append(g)
|
||||
# Increment the position
|
||||
pos += posi
|
||||
if pos.x > self.rm or pos.y > self.bm:
|
||||
break
|
||||
|
||||
def undraw(self, board):
|
||||
for e in self.pcb_items:
|
||||
board.Remove(e)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
( page_layout
|
||||
( setup (textsize 1.5 1.5) (linewidth 0.15) (textlinewidth 0.15)
|
||||
(left_margin 10) (right_margin 10) (top_margin 10) (bottom_margin 10) )
|
||||
( rect (comment "rect around the title block") (linewidth 0.15) (start 110 34) (end 2 2) )
|
||||
( rect (start 0 0 ltcorner) (end 0 0 rbcorner) (repeat 2) (incrx 2) (incry 2) )
|
||||
( line (start 50 2 ltcorner) (end 50 0 ltcorner) (repeat 30) (incrx 50) )
|
||||
( tbtext "1" (pos 25 1 ltcorner) (font (size 1.3 1.3))(repeat 100) (incrx 50) )
|
||||
( line (start 50 2 lbcorner) (end 50 0 lbcorner) (repeat 30) (incrx 50) )
|
||||
( tbtext "1" (pos 25 1 lbcorner) (font (size 1.3 1.3)) (repeat 100) (incrx 50) )
|
||||
( line (start 0 50 ltcorner) (end 2 50 ltcorner) (repeat 30) (incry 50) )
|
||||
( tbtext "A" (pos 1 25 ltcorner) (font (size 1.3 1.3))
|
||||
(justify center)(repeat 100) (incry 50) )
|
||||
( line (start 0 50 rtcorner) (end 2 50 rtcorner) (repeat 30) (incry 50) )
|
||||
( tbtext "A" (pos 1 25 rtcorner) (font (size 1.3 1.3))
|
||||
(justify center) (repeat 100) (incry 50) )
|
||||
( tbtext "Date: %D" (pos 87 6.9) )
|
||||
( line (start 110 5.5) (end 2 5.5) )
|
||||
( tbtext "%K" (pos 109 4.1) (comment "Kicad version" ) )
|
||||
( line (start 110 8.5) (end 2 8.5) )
|
||||
( tbtext "Rev: %R" (pos 24 6.9)(font bold)(justify left) )
|
||||
( tbtext "Size: %Z" (comment "Paper format name")(pos 109 6.9) )
|
||||
( tbtext "Id: %S/%N" (comment "Sheet id")(pos 24 4.1) )
|
||||
( line (start 110 12.5) (end 2 12.5) )
|
||||
( tbtext "Title: %T" (pos 109 10.7)(font bold italic (size 2 2)) )
|
||||
( tbtext "File: %F" (pos 109 14.3) )
|
||||
( line (start 110 18.5) (end 2 18.5) )
|
||||
( tbtext "Sheet: %P" (pos 109 17) )
|
||||
( tbtext "%Y" (comment "Company name") (pos 109 20)(font bold) )
|
||||
( tbtext "%C0" (comment "Comment 0") (pos 109 23) )
|
||||
( tbtext "%C1" (comment "Comment 1") (pos 109 26) )
|
||||
( tbtext "%C2" (comment "Comment 2") (pos 109 29) )
|
||||
( tbtext "%C3" (comment "Comment 3") (pos 109 32) )
|
||||
( line (start 90 8.5) (end 90 5.5) )
|
||||
( line (start 26 8.5) (end 26 2) )
|
||||
)
|
||||
|
|
@ -232,6 +232,7 @@ W_UNKVAR = '(W082) '
|
|||
W_WRONGEXT = '(W083) '
|
||||
W_COLORTHEME = '(W084) '
|
||||
W_WRONGCOLOR = '(W085) '
|
||||
W_WKSVERSION = '(W086) '
|
||||
# Somehow arbitrary, the colors are real, but can be different
|
||||
PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"}
|
||||
PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e",
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@ from .optionable import Optionable
|
|||
from .out_base import VariantOptions
|
||||
from .kicad.color_theme import load_color_theme
|
||||
from .kicad.patch_svg import patch_svg_file
|
||||
from .kicad.worksheet import Worksheet
|
||||
from .kicad.config import KiConf
|
||||
from .kicad.pcb import PCB
|
||||
from .misc import CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT, MISSING_TOOL
|
||||
from .kiplot import check_script, exec_with_retry, add_extra_options
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from .layer import Layer, get_priority
|
||||
from .__main__ import __version__
|
||||
from . import PyPDF2
|
||||
from . import log
|
||||
|
||||
|
|
@ -32,10 +36,8 @@ VIATYPE_THROUGH = 3
|
|||
VIATYPE_BLIND_BURIED = 2
|
||||
VIATYPE_MICROVIA = 1
|
||||
|
||||
|
||||
# - Use PyPDF2 for pdfunite
|
||||
# - Analyze KiCad 6 long delay
|
||||
# - Manually draw the frame
|
||||
|
||||
|
||||
def _run_command(cmd):
|
||||
|
|
@ -107,32 +109,6 @@ def to_inches(w):
|
|||
return val
|
||||
|
||||
|
||||
def merge_svg(input_folder, input_files, output_folder, output_file, colored_holes, holes_color, monochrome):
|
||||
""" Merge all pages into one """
|
||||
first = True
|
||||
for (file, color) in input_files:
|
||||
file = os.path.join(input_folder, file)
|
||||
new_layer = fromstring(load_svg(file, color, colored_holes, holes_color, monochrome))
|
||||
width = get_width(new_layer)
|
||||
if first:
|
||||
svg_out = new_layer
|
||||
# This is the width declared at the beginning of the file
|
||||
base_width = width
|
||||
phys_width = to_inches(new_layer.width)
|
||||
first = False
|
||||
else:
|
||||
root = new_layer.getroot()
|
||||
# Adjust the coordinates of this section to the main width
|
||||
scale = base_width/width
|
||||
if scale != 1.0:
|
||||
logger.debug(' - Scaling {} by {}'.format(file, scale))
|
||||
for e in root:
|
||||
e.scale(scale)
|
||||
svg_out.append([root])
|
||||
svg_out.save(os.path.join(output_folder, output_file))
|
||||
return phys_width
|
||||
|
||||
|
||||
def create_pdf_from_pages(input_folder, input_files, output_fn):
|
||||
output = PyPDF2.PdfFileWriter()
|
||||
# Collect all pages
|
||||
|
|
@ -295,10 +271,15 @@ class PCB_PrintOptions(VariantOptions):
|
|||
Usually user colors are stored as `user`, but you can give it another name """
|
||||
self.plot_sheet_reference = True
|
||||
""" Include the title-block """
|
||||
self.enable_ki6_frame_fix = False
|
||||
""" KiCad 6 doesn't support custom title-block/frames from Python.
|
||||
This option uses KiCad GUI to print the frame, is slow, but works.
|
||||
Always enabled for KiCad 5, which crashes if we try to plot the frame """
|
||||
self.frame_plot_mechanism = 'internal'
|
||||
""" [gui,internal,plot] Plotting the frame from Python is problematic.
|
||||
This option selects a workaround strategy.
|
||||
gui: uses KiCad GUI to do it. Is slow but you get the correct frame.
|
||||
But it can't keep track of page numbers.
|
||||
internal: KiBot loads the `.kicad_wks` and does the drawing work.
|
||||
Best option, but some details are different from what the GUI generates.
|
||||
plot: uses KiCad Python API. Only available for KiCad 6.
|
||||
You get the default frame and some substitutions doesn't work """
|
||||
self.pages = PagesOptions
|
||||
""" [list(dict)] List of pages to include in the output document.
|
||||
Each page contains one or more layers of the PCB """
|
||||
|
|
@ -360,6 +341,8 @@ class PCB_PrintOptions(VariantOptions):
|
|||
self.validate_color(member)
|
||||
else:
|
||||
setattr(self, member, getattr(self._color_theme, color))
|
||||
if self.frame_plot_mechanism == 'plot' and GS.ki5():
|
||||
raise KiPlotConfigurationError("You can't use `plot` for `frame_plot_mechanism` with KiCad 5. It will crash.")
|
||||
|
||||
def filter_components(self):
|
||||
if not self._comps:
|
||||
|
|
@ -408,7 +391,7 @@ class PCB_PrintOptions(VariantOptions):
|
|||
for g in self.moved_items:
|
||||
g.SetLayer(self.cleared_layer)
|
||||
|
||||
def plot_frame_ki6(self, pc, po, p):
|
||||
def plot_frame_api(self, pc, po, p):
|
||||
""" KiCad 6 can plot the frame because it loads the worksheet format.
|
||||
But not the one from the project, just a default """
|
||||
self.clear_layer('Edge.Cuts')
|
||||
|
|
@ -420,7 +403,49 @@ class PCB_PrintOptions(VariantOptions):
|
|||
pc.PlotLayer()
|
||||
self.restore_layer()
|
||||
|
||||
def plot_frame_ki5(self, dir_name, layer='Edge.Cuts'):
|
||||
def fill_kicad_vars(self, page, pages, p):
|
||||
vars = {}
|
||||
vars['KICAD_VERSION'] = 'KiCad E.D.A. '+GS.kicad_version+' + KiBot v'+__version__
|
||||
vars['#'] = str(page)
|
||||
vars['##'] = str(pages)
|
||||
GS.load_pcb_title_block()
|
||||
for num in range(9):
|
||||
vars['COMMENT'+str(num+1)] = GS.pcb_com[num]
|
||||
vars['COMPANY'] = GS.pcb_comp
|
||||
vars['ISSUE_DATE'] = GS.pcb_date
|
||||
vars['REVISION'] = GS.pcb_rev
|
||||
# The set_title member already took care of modifying the board value
|
||||
tb = GS.board.GetTitleBlock()
|
||||
vars['TITLE'] = tb.GetTitle()
|
||||
vars['FILENAME'] = GS.pcb_basename+'.kicad_pcb'
|
||||
vars['SHEETNAME'] = p.sheet
|
||||
layer = ''
|
||||
for la in p.layers:
|
||||
if len(layer):
|
||||
layer += '+'
|
||||
layer = layer+la.layer
|
||||
vars['LAYER'] = layer
|
||||
vars['PAPER'] = self.paper
|
||||
return vars
|
||||
|
||||
def plot_frame_internal(self, pc, po, p, page, pages):
|
||||
""" Here we plot the frame manually """
|
||||
self.clear_layer('Edge.Cuts')
|
||||
po.SetPlotFrameRef(False)
|
||||
po.SetScale(1.0)
|
||||
po.SetNegative(False)
|
||||
pc.SetLayer(self.cleared_layer)
|
||||
ws = Worksheet.load(self.layout)
|
||||
tb_vars = self.fill_kicad_vars(page, pages, p)
|
||||
ws.draw(GS.board, self.cleared_layer, page, self.paper_w, self.paper_h, tb_vars)
|
||||
pc.OpenPlotfile('frame', PLOT_FORMAT_SVG, p.sheet)
|
||||
pc.PlotLayer()
|
||||
ws.undraw(GS.board)
|
||||
self.restore_layer()
|
||||
# We need to plot the images in a separated pass
|
||||
self.last_worksheet = ws
|
||||
|
||||
def plot_frame_gui(self, dir_name, layer='Edge.Cuts'):
|
||||
""" KiCad 5 crashes if we try to print the frame.
|
||||
So we print a frame using pcbnew_do export.
|
||||
We use SVG output to then generate a vectorized PDF. """
|
||||
|
|
@ -576,6 +601,43 @@ class PCB_PrintOptions(VariantOptions):
|
|||
# Add it to the list
|
||||
filelist.append((GS.pcb_basename+"-"+suffix+".svg", via_c))
|
||||
|
||||
def add_frame_images(self, svg):
|
||||
if not self.frame_plot_mechanism == 'internal' or not self.last_worksheet.has_images:
|
||||
return
|
||||
self.last_worksheet.add_images_to_svg(svg)
|
||||
|
||||
def merge_svg(self, input_folder, input_files, output_folder, output_file, p):
|
||||
""" Merge all pages into one """
|
||||
first = True
|
||||
for (file, color) in input_files:
|
||||
file = os.path.join(input_folder, file)
|
||||
new_layer = fromstring(load_svg(file, color, p.colored_holes, p.holes_color, p.monochrome))
|
||||
width = get_width(new_layer)
|
||||
if first:
|
||||
svg_out = new_layer
|
||||
# This is the width declared at the beginning of the file
|
||||
base_width = width
|
||||
phys_width = to_inches(new_layer.width)
|
||||
first = False
|
||||
self.add_frame_images(svg_out)
|
||||
else:
|
||||
root = new_layer.getroot()
|
||||
# Adjust the coordinates of this section to the main width
|
||||
scale = base_width/width
|
||||
if scale != 1.0:
|
||||
logger.debug(' - Scaling {} by {}'.format(file, scale))
|
||||
for e in root:
|
||||
e.scale(scale)
|
||||
svg_out.append([root])
|
||||
svg_out.save(os.path.join(output_folder, output_file))
|
||||
return phys_width
|
||||
|
||||
def find_paper_size(self):
|
||||
pcb = PCB.load(GS.pcb_file)
|
||||
self.paper_w = pcb.paper_w
|
||||
self.paper_h = pcb.paper_h
|
||||
self.paper = pcb.paper
|
||||
|
||||
def generate_output(self, output):
|
||||
if self.format != 'SVG' and which(SVG2PDF) is None:
|
||||
logger.error('`{}` not installed. Install `librsvg2-bin` or equivalent'.format(SVG2PDF))
|
||||
|
|
@ -590,6 +652,13 @@ class PCB_PrintOptions(VariantOptions):
|
|||
else:
|
||||
temp_dir_base = mkdtemp(prefix='tmp-kibot-pcb_print-')
|
||||
logger.debug('- Temporal dir: {}'.format(temp_dir_base))
|
||||
self.find_paper_size()
|
||||
# Find the layout file
|
||||
layout = KiConf.fix_page_layout(GS.pro_file, dry=True)[1]
|
||||
if not layout or not os.path.isfile(layout):
|
||||
layout = os.path.abspath(os.path.join(os.path.dirname(__file__), 'kicad_layouts', 'default.kicad_wks'))
|
||||
logger.debug('- Using layout: '+layout)
|
||||
self.layout = layout
|
||||
# Plot options
|
||||
pc = PLOT_CONTROLLER(GS.board)
|
||||
po = pc.GetPlotOptions()
|
||||
|
|
@ -639,13 +708,12 @@ class PCB_PrintOptions(VariantOptions):
|
|||
# 2) Plot the frame using an empty layer and 1.0 scale
|
||||
if self.plot_sheet_reference:
|
||||
logger.debug('- Plotting the frame')
|
||||
if GS.ki6():
|
||||
if self.enable_ki6_frame_fix:
|
||||
self.plot_frame_ki5(temp_dir)
|
||||
else:
|
||||
self.plot_frame_ki6(pc, po, p)
|
||||
else:
|
||||
self.plot_frame_ki5(temp_dir)
|
||||
if self.frame_plot_mechanism == 'gui':
|
||||
self.plot_frame_gui(temp_dir)
|
||||
elif self.frame_plot_mechanism == 'plot':
|
||||
self.plot_frame_api(pc, po, p)
|
||||
else: # internal
|
||||
self.plot_frame_internal(pc, po, p, len(pages)+1, len(self.pages))
|
||||
color = p.sheet_reference_color if p.sheet_reference_color else self._color_theme.pcb_frame
|
||||
filelist.append((GS.pcb_basename+"-frame.svg", color))
|
||||
pc.ClosePlot()
|
||||
|
|
@ -656,7 +724,7 @@ class PCB_PrintOptions(VariantOptions):
|
|||
else:
|
||||
assembly_file = GS.pcb_basename+".svg"
|
||||
logger.debug('- Merging layers to {}'.format(assembly_file))
|
||||
merge_svg(temp_dir, filelist, temp_dir, assembly_file, p.colored_holes, p.holes_color, p.monochrome)
|
||||
self.merge_svg(temp_dir, filelist, temp_dir, assembly_file, p)
|
||||
if self.format in ['PNG', 'EPS']:
|
||||
id = self._expand_id+('_page_'+page_str)
|
||||
out_file = self.expand_filename(output_dir, self.output, id, self._expand_ext)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Example KiBot config file
|
||||
kibot:
|
||||
version: 1
|
||||
|
||||
outputs:
|
||||
- name: 'print_front'
|
||||
comment: "Experiment"
|
||||
type: pcb_print
|
||||
dir: Layers
|
||||
options:
|
||||
# title: 'Fake title for front copper and silk'
|
||||
# color_theme: _builtin_default
|
||||
# drill_marks: small
|
||||
title: Chau
|
||||
plot_sheet_reference: true
|
||||
format: 'PDF'
|
||||
keep_temporal_files: true
|
||||
# enable_ki6_frame_fix: true
|
||||
pages:
|
||||
- layers:
|
||||
- layer: Edge.Cuts
|
||||
color: "#004040"
|
||||
Loading…
Reference in New Issue