Skip to content

Commit 2fee6dc

Browse files
committed
[Drill][KiCad 10][Added] Support for kicad-cli
This mitigates KiCad bug: https://gitlab.com/kicad/code/kicad/-/work_items/23268 Now we use the Python API when left/right digits are specified
1 parent e147e6e commit 2fee6dc

4 files changed

Lines changed: 105 additions & 40 deletions

File tree

kibot/out_any_drill.py

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (c) 2020-2025 Salvador E. Tropea
3-
# Copyright (c) 2020-2025 Instituto Nacional de Tecnología Industrial
2+
# Copyright (c) 2020-2026 Salvador E. Tropea
3+
# Copyright (c) 2020-2026 Instituto Nacional de Tecnología Industrial
44
# License: AGPL-3.0
55
# Project: KiBot (formerly KiPlot)
66
# Drill table contributed by Nguyen Vincent (@nguyen-v)
@@ -11,6 +11,7 @@
1111
PLOT_FORMAT_PDF, wxPoint, B_Cu, F_Cu, GERBER_WRITER)
1212
from .error import KiPlotConfigurationError
1313
from .kicad.drill_info import get_full_holes_list, PLATED_DICT, HOLE_SHAPE_DICT, HOLE_TYPE_DICT
14+
from .kiplot import run_command
1415
from .optionable import Optionable
1516
from .out_base import VariantOptions
1617
from .gs import GS
@@ -166,8 +167,14 @@ def config(self, parent):
166167
else:
167168
map = self.map.type
168169
self._map_output = self.map.output
169-
if GS.ki10 and map == 'hpgl':
170-
raise KiPlotConfigurationError("KiCad 10+ doesn't support HPGL")
170+
if GS.ki10:
171+
if map == 'hpgl':
172+
raise KiPlotConfigurationError("KiCad 10+ doesn't support HPGL")
173+
if map == 'gerber':
174+
self._map_cli = 'gerberx2'
175+
else:
176+
self._map_cli = map
177+
self._map_type = map
171178
self._map_ext = self._map_ext[map]
172179
self._map = self._map_map[map]
173180
# Solve the report for both cases
@@ -260,12 +267,17 @@ def _get_drill_groups(unified):
260267
groups.extend(list(pairs))
261268
return groups
262269

263-
def get_file_names(self, output_dir):
270+
def get_file_names(self, output_dir, just_drills=False, tmp_base=None):
264271
""" Returns a dict containing KiCad names and its replacement.
265-
If no replacement is needed the replacement is empty """
272+
If no replacement is needed the replacement is empty.
273+
tmp_base is used to specify the `GS.pcb_basename` for a temporal variant.
274+
I don't really know if variants really apply to drill files, but is currently supported """
266275
filenames = {}
267276
self._configure_writer(GS.board, wxPoint(0, 0))
268277
files = AnyDrill._get_drill_groups(self._unified_output)
278+
force_rename = tmp_base is not None
279+
if not force_rename:
280+
tmp_base = GS.pcb_basename
269281
for d in files:
270282
kicad_id = '-'+d if d else d
271283
kibot_id = self.solve_id(d)
@@ -274,17 +286,22 @@ def get_file_names(self, output_dir):
274286
kicad_id_main += '-drl'
275287
if not GS.ki8:
276288
kicad_id_map = kicad_id_main
277-
if self.generate_drill_files:
278-
k_file = self.expand_filename(output_dir, '%f'+kicad_id_main+'.%x', '', self._ext)
289+
if self.generate_drill_files or just_drills:
290+
k_file = self.expand_filename(output_dir, tmp_base+kicad_id_main+'.%x', '', self._ext)
279291
file = ''
280292
if self.output:
281293
file = self.expand_filename(output_dir, self.output, kibot_id, self._ext)
294+
elif force_rename:
295+
# To get the real name instead of the temporal one
296+
file = k_file.replace(tmp_base, GS.pcb_basename)
282297
filenames[k_file] = file
283-
if self._map is not None:
284-
k_file = self.expand_filename(output_dir, '%f'+kicad_id_map+'-drl_map.%x', '', self._map_ext)
298+
if self._map is not None and not just_drills:
299+
k_file = self.expand_filename(output_dir, tmp_base+kicad_id_map+'-drl_map.%x', '', self._map_ext)
285300
file = ''
286301
if self._map_output:
287302
file = self.expand_filename(output_dir, self._map_output, kibot_id+'_map', self._map_ext)
303+
elif force_rename:
304+
file = k_file.replace(tmp_base, GS.pcb_basename)
288305
filenames[k_file] = file
289306
return filenames
290307

@@ -298,43 +315,70 @@ def run(self, output_dir):
298315
offset = GS.get_aux_origin()
299316
else:
300317
offset = wxPoint(0, 0)
301-
drill_writer = self._configure_writer(GS.board, offset)
318+
drill_writer, use_cli = self._configure_writer(GS.board, offset)
302319

303320
logger.debug("Generating drill files in "+output_dir)
304321
gen_map = self._map is not None
305-
if gen_map:
306-
drill_writer.SetMapFileFormat(self._map)
307-
logger.debug("Generating drill map type {} in {}".format(self._map, output_dir))
322+
308323
if not self.generate_drill_files and not gen_map and not self._report and not self._table_output:
309324
logger.warning(
310325
W_NODRILL +
311326
"Not generating drill files nor drill maps "
312327
"nor report nor drill table on "
313328
f"`{self._parent.name}`"
314329
)
315-
if GS.ki10 and isinstance(drill_writer, GERBER_WRITER):
316-
# KiCad 10 inconsistency ...
317-
drill_writer.CreateDrillandMapFilesSet(output_dir, self.generate_drill_files, gen_map, True)
330+
331+
if use_cli:
332+
# Using kicad-cli
333+
fname = self.save_tmp_board() if self.will_filter_pcb_components() else GS.pcb_file
334+
odir = output_dir # if self.generate_drill_files else tempfile.gettempdir()
335+
cmd = [GS.kicad_cli, 'pcb', 'export', 'drill', '--output', odir] + drill_writer
336+
if gen_map:
337+
logger.debug("Generating drill map type {} in {}".format(self._map, output_dir))
338+
cmd.extend(['--generate-map', '--map-format', self._map_cli])
339+
if self._report:
340+
drill_report_file = self.expand_filename(output_dir, self._report, 'drill_report', 'txt')
341+
logger.debug("Generating drill report: "+drill_report_file)
342+
cmd.extend(['--generate-report', '--report-path', drill_report_file])
343+
run_command(cmd + [fname])
344+
if self._files_to_remove:
345+
self.remove_temporals()
346+
tmp_base = os.path.splitext(os.path.basename(fname))[0]
347+
if not self.generate_drill_files:
348+
# We can't prevent kicad-cli from generating them, so just remove them
349+
files = self.get_file_names(output_dir, tmp_base=tmp_base, just_drills=True)
350+
for f in files.keys():
351+
os.remove(f)
318352
else:
319-
drill_writer.CreateDrillandMapFilesSet(output_dir, self.generate_drill_files, gen_map)
353+
# Using Python API
354+
if gen_map:
355+
drill_writer.SetMapFileFormat(self._map)
356+
logger.debug("Generating drill map type {} in {}".format(self._map, output_dir))
357+
if GS.ki10 and isinstance(drill_writer, GERBER_WRITER):
358+
# KiCad 10 inconsistency ...
359+
drill_writer.CreateDrillandMapFilesSet(output_dir, self.generate_drill_files, gen_map, True)
360+
else:
361+
drill_writer.CreateDrillandMapFilesSet(output_dir, self.generate_drill_files, gen_map)
362+
# Generate the report
363+
if self._report:
364+
drill_report_file = self.expand_filename(output_dir, self._report, 'drill_report', 'txt')
365+
logger.debug("Generating drill report: "+drill_report_file)
366+
if GS.ki10:
367+
logger.error("No drill reports until KiCad bug https://gitlab.com/kicad/code/kicad/-/work_items/23268 "
368+
"is fixed")
369+
else:
370+
drill_writer.GenDrillReportFile(drill_report_file)
371+
tmp_base = None
372+
320373
# Rename the files
321-
files = self.get_file_names(output_dir)
374+
files = self.get_file_names(output_dir, tmp_base=tmp_base)
322375
for k_f, f in files.items():
323376
if f:
324377
logger.debug(f"Renaming {k_f} -> {f}")
325378
if not os.path.isfile(k_f):
326379
GS.exit_with_error(f"Missing `{k_f}` drill file, KiCad bug? please report", DONT_STOP)
327380
else:
328381
os.replace(k_f, f)
329-
# Generate the report
330-
if self._report:
331-
drill_report_file = self.expand_filename(output_dir, self._report, 'drill_report', 'txt')
332-
logger.debug("Generating drill report: "+drill_report_file)
333-
if GS.ki10:
334-
logger.error("No drill reports until KiCad bug https://gitlab.com/kicad/code/kicad/-/work_items/23268 "
335-
"is fixed")
336-
else:
337-
drill_writer.GenDrillReportFile(drill_report_file)
338382
# Generate the drill table
339383
if self._table_output:
340384

kibot/out_excellon.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (c) 2020-2023 Salvador E. Tropea
3-
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
4-
# License: GPL-3.0
2+
# Copyright (c) 2020-2026 Salvador E. Tropea
3+
# Copyright (c) 2020-2026 Instituto Nacional de Tecnología Industrial
4+
# License: AGPL-3.0
55
# Project: KiBot (formerly KiPlot)
66
from pcbnew import EXCELLON_WRITER
77
from .out_any_drill import AnyDrill
@@ -12,6 +12,10 @@
1212
'SUPPRESS_LEADING': EXCELLON_WRITER.SUPPRESS_LEADING,
1313
'SUPPRESS_TRAILING': EXCELLON_WRITER.SUPPRESS_TRAILING,
1414
'KEEP_ZEROS': EXCELLON_WRITER.KEEP_ZEROS}
15+
ZF_CLI = {'DECIMAL_FORMAT': 'decimal',
16+
'SUPPRESS_LEADING': 'suppressleading',
17+
'SUPPRESS_TRAILING': 'suppresstrailing',
18+
'KEEP_ZEROS': 'keep'}
1519

1620

1721
class ExcellonOptions(AnyDrill):
@@ -37,12 +41,26 @@ def __init__(self):
3741
self._ext = 'drl'
3842

3943
def _configure_writer(self, board, offset):
44+
self._unified_output = self.pth_and_npth_single_file
45+
if GS.ki10 and self.left_digits == 0 and self.right_digits == 0:
46+
options = ['--format', 'excellon',
47+
'--excellon-units', 'mm' if self.metric_units else 'in',
48+
'--excellon-zeros-format', ZF_CLI[self.zeros_format],
49+
'--excellon-oval-format', 'route' if self.route_mode_for_oval_holes else 'alternate',
50+
'--drill-origin', 'plot' if offset.x != 0 or offset.y != 0 else 'absolute']
51+
if not self.pth_and_npth_single_file:
52+
options.append('--excellon-separate-th')
53+
if self.minimal_header:
54+
options.append('--excellon-min-header')
55+
if self.mirror_y_axis:
56+
options.append('--excellon-mirror-y')
57+
return options, True
58+
# KiCad <10 or left_digits/right_digits
4059
drill_writer = EXCELLON_WRITER(board)
4160
drill_writer.SetOptions(self.mirror_y_axis, self.minimal_header, GS.p2v_k7(offset), self.pth_and_npth_single_file)
4261
drill_writer.SetRouteModeForOvalHoles(self.route_mode_for_oval_holes)
4362
drill_writer.SetFormat(self.metric_units, ZF[self.zeros_format], self.left_digits, self.right_digits)
44-
self._unified_output = self.pth_and_npth_single_file
45-
return drill_writer
63+
return drill_writer, False
4664

4765

4866
@output_class

kibot/out_gerb_drill.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (c) 2020-2023 Salvador E. Tropea
3-
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
4-
# License: GPL-3.0
2+
# Copyright (c) 2020-2026 Salvador E. Tropea
3+
# Copyright (c) 2020-2026 Instituto Nacional de Tecnología Industrial
4+
# License: AGPL-3.0
55
# Project: KiBot (formerly KiPlot)
66
from pcbnew import GERBER_WRITER
77
from .gs import GS
@@ -15,11 +15,16 @@ def __init__(self):
1515
self._ext = 'gbr'
1616

1717
def _configure_writer(self, board, offset):
18+
if GS.ki10:
19+
options = ['--format', 'gerber',
20+
'--drill-origin', 'plot' if offset.x != 0 or offset.y != 0 else 'absolute',
21+
'--gerber-precision', '6'] # Currently is always 4.6
22+
return options, True
1823
drill_writer = GERBER_WRITER(board)
1924
# hard coded in UI?
2025
drill_writer.SetFormat(5)
2126
drill_writer.SetOptions(GS.p2v_k7(offset))
22-
return drill_writer
27+
return drill_writer, False
2328

2429

2530
@output_class

tests/test_plot/test_drill.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ def do_3Rs(test_dir, conf, modern, single=False):
8282
npth_pdf_drl = npth_pdf_drl.replace('-NPTH', '')
8383
npth_csv_drl = npth_csv_drl.replace('_NPTH', '')
8484

85-
if not context.ki10():
86-
# KiCad 10 bug https://gitlab.com/kicad/code/kicad/-/work_items/23268
87-
ctx.expect_out_file(os.path.join(DRILL_DIR, report))
85+
ctx.expect_out_file(os.path.join(DRILL_DIR, report))
8886
ctx.expect_out_file(pth_drl)
8987
ctx.expect_out_file(npth_drl)
9088
ctx.expect_out_file(f1_drl)

0 commit comments

Comments
 (0)