Skip to content

Add support for plotting position files #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/kiplot/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,24 @@ def _parse_out_opts(self, otype, options):
'to': 'mirror_y_axis',
'required': lambda opts: True,
},
{
'key': 'format',
'types': ['position'],
'to': 'format',
'required': lambda opts: True,
},
{
'key': 'units',
'types': ['position'],
'to': 'units',
'required': lambda opts: True,
},
{
'key': 'separate_files_for_front_and_back',
'types': ['position'],
'to': 'separate_files_for_front_and_back',
'required': lambda opts: True,
}
]

po = PC.OutputOptions(otype)
Expand Down Expand Up @@ -411,7 +429,7 @@ def _parse_output(self, o_obj):
raise YamlError("Output needs a type")

if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg',
'gerb_drill', 'excellon']:
'gerb_drill', 'excellon', 'position']:
raise YamlError("Unknown output type: {}".format(otype))

try:
Expand Down
166 changes: 166 additions & 0 deletions src/kiplot/kiplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Main Kiplot code
"""

from datetime import datetime
import logging
import os

Expand Down Expand Up @@ -51,6 +52,8 @@ def plot(self, brd_file):
self._do_layer_plot(board, pc, op)
elif self._output_is_drill(op):
self._do_drill_plot(board, pc, op)
elif self._output_is_position(op):
self._do_position_plot(board, pc, op)
else:
raise PlotError("Don't know how to plot type {}"
.format(op.options.type))
Expand Down Expand Up @@ -85,6 +88,9 @@ def _output_is_drill(self, output):
PCfg.OutputOptions.GERB_DRILL,
]

def _output_is_position(self, output):
return output.options.type == PCfg.OutputOptions.POSITION

def _get_layer_plot_format(self, output):
"""
Gets the Pcbnew plot format for a given KiPlot output type
Expand Down Expand Up @@ -221,6 +227,160 @@ def _do_drill_plot(self, board, plot_ctrl, output):

drill_writer.GenDrillReportFile(drill_report_file)

def _do_position_plot_ascii(self, board, plot_ctrl, output, columns, modulesStr, maxSizes):
to = output.options.type_options
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
if not os.path.exists(outdir):
os.makedirs(outdir)
name = os.path.splitext(os.path.basename(board.GetFileName()))[0]

topf = None
botf = None
bothf = None
if to.separate_files_for_front_and_back:
topf = open(os.path.join(outdir, "{}-top.pos".format(name)), 'w')
botf = open(os.path.join(outdir, "{}-bottom.pos".format(name)),
'w')
else:
bothf = open(os.path.join(outdir, "{}-both.pos").format(name), 'w')

files = [f for f in [topf, botf, bothf] if f is not None]
for f in files:
f.write('### Module positions - created on {} ###\n'.format(
datetime.now().strftime("%a %d %b %Y %I:%M:%S %p %Z")
))
f.write('### Printed by KiPlot\n')
unit = {'millimeters': 'mm',
'inches': 'in'}[to.units]
f.write('## Unit: {}, Angle = deg\n'.format(unit))

if topf is not None:
topf.write('## Side: top\n')
if botf is not None:
botf.write('## Side: bottom\n')
if bothf is not None:
bothf.write('## Side: both\n')

for f in files:
f.write('# ')
for idx, col in enumerate(columns):
if idx > 0:
f.write(" ")
f.write("{0: <{width}}".format(col, width=maxSizes[idx]))
f.write('\n')

# Account for the "# " at the start of the comment column
maxSizes[0] = maxSizes[0] + 2

for m in modulesStr:
fle = bothf
if fle is None:
if m[-1] == "top":
fle = topf
else:
fle = botf
for idx, col in enumerate(m):
if idx > 0:
fle.write(" ")
fle.write("{0: <{width}}".format(col, width=maxSizes[idx]))
fle.write("\n")

for f in files:
f.write("## End\n")

if topf is not None:
topf.close()
if botf is not None:
botf.close()
if bothf is not None:
bothf.close()

def _do_position_plot_csv(self, board, plot_ctrl, output, columns, modulesStr):
to = output.options.type_options
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
if not os.path.exists(outdir):
os.makedirs(outdir)
name = os.path.splitext(os.path.basename(board.GetFileName()))[0]

topf = None
botf = None
bothf = None
if to.separate_files_for_front_and_back:
topf = open(os.path.join(outdir, "{}-top-pos.csv".format(name)),
'w')
botf = open(os.path.join(outdir, "{}-bottom-pos.csv".format(name)),
'w')
else:
bothf = open(os.path.join(outdir, "{}-both-pos.csv").format(name),
'w')

files = [f for f in [topf, botf, bothf] if f is not None]

for f in files:
f.write(",".join(columns))
f.write("\n")

for m in modulesStr:
fle = bothf
if fle is None:
if m[-1] == "top":
fle = topf
else:
fle = botf
fle.write(",".join('"{}"'.format(e) for e in m))
fle.write("\n")

if topf is not None:
topf.close()
if botf is not None:
botf.close()
if bothf is not None:
bothf.close()

def _do_position_plot(self, board, plot_ctrl, output):
to = output.options.type_options

columns = ["ref", "val", "package", "posx", "posy", "rot", "side"]
colcount = len(columns)

conv = 1.0
if to.units == 'millimeters':
conv = 1.0 / pcbnew.IU_PER_MM
elif to.units == 'inches':
conv = 0.001 / pcbnew.IU_PER_MILS
else:
raise PlotError('Invalid units: {}'.format(to.units))

# Format all strings
modules = []
for m in board.GetModules():
center = m.GetCenter()
# See PLACE_FILE_EXPORTER::GenPositionData() in
# export_footprints_placefile.cpp for C++ version of this.
modules.append([
"{}".format(m.GetReference()),
"{}".format(m.GetValue()),
"{}".format(m.GetFPID().GetLibItemName()),
"{:.4f}".format(center.x * conv),
"{:.4f}".format(center.y * conv),
"{:.4f}".format(m.GetOrientationDegrees()),
"{}".format("bottom" if m.IsFlipped() else "top")
])

# Find max width for all columns
maxlengths = [0] * colcount
for row in range(len(modules)):
for col in range(colcount):
maxlengths[col] = max(maxlengths[col], len(modules[row][col]))

if to.format.lower() == 'ascii':
self._do_position_plot_ascii(board, plot_ctrl, output, columns, modules,
maxlengths)
elif to.format.lower() == 'csv':
self._do_position_plot_csv(board, plot_ctrl, output, columns, modules)
else:
raise PlotError("Format is invalid: {}".format(to.format))

def _configure_gerber_opts(self, po, output):

# true if gerber
Expand Down Expand Up @@ -271,6 +431,10 @@ def _configure_svg_opts(self, po, output):
assert(output.options.type == PCfg.OutputOptions.SVG)
# pdf_opts = output.options.type_options

def _configure_position_opts(self, po, output):

assert(output.options.type == PCfg.OutputOptions.POSITION)

def _configure_output_dir(self, plot_ctrl, output):

po = plot_ctrl.GetPlotOptions()
Expand Down Expand Up @@ -324,6 +488,8 @@ def _configure_plot_ctrl(self, plot_ctrl, output):
self._configure_pdf_opts(po, output)
elif output.options.type == PCfg.OutputOptions.HPGL:
self._configure_hpgl_opts(po, output)
elif output.options.type == PCfg.OutputOptions.POSITION:
self._configure_position_opts(po, output)

po.SetDrillMarksType(opts.drill_marks)

Expand Down
19 changes: 19 additions & 0 deletions src/kiplot/plot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,22 @@ def __init__(self):
self.type = None


class PositionOptions(TypeOptions):

def __init__(self):
self.format = None
self.units = None
self.separate_files_for_front_and_back = None

def validate(self):
errs = []
if self.format not in ["ASCII", "CSV"]:
errs.append("Format must be either ASCII or CSV")
if self.units not in ["millimeters", "inches"]:
errs.append("Units must be either millimeters or inches")
return errs


class OutputOptions(object):

GERBER = 'gerber'
Expand All @@ -372,6 +388,7 @@ class OutputOptions(object):

EXCELLON = 'excellon'
GERB_DRILL = 'gerb_drill'
POSITION = 'position'

def __init__(self, otype):
self.type = otype
Expand All @@ -392,6 +409,8 @@ def __init__(self, otype):
self.type_options = ExcellonOptions()
elif otype == self.GERB_DRILL:
self.type_options = GerberDrillOptions()
elif otype == self.POSITION:
self.type_options = PositionOptions()
else:
self.type_options = None

Expand Down
11 changes: 10 additions & 1 deletion tests/yaml_samples/simple_2layer.kiplot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ outputs:
- layer: F.Cu
suffix: F_Cu
- layer: F.SilkS
suffix: F_SilkS
suffix: F_SilkS

- name: 'position'
comment: "Pick and place file"
type: position
dir: positiondir
options:
format: ASCII # CSV or ASCII format
units: millimeters # millimeters or inches
separate_files_for_front_and_back: true