Skip to content

Commit

Permalink
Merge pull request #2 from INTI-CMNB/step_output
Browse files Browse the repository at this point in the history
3D STEP output added
  • Loading branch information
set-soft authored Jun 15, 2020
2 parents 8fbcffb + 7df1e9f commit 7136662
Show file tree
Hide file tree
Showing 29 changed files with 303 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


## [Unreleased]
### Added
- STEP 3D model generation

## [0.3.0] - 2020-06-14
### Added
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For example, it's common that you might want for each board rev:
* Gerbers, drills and drill maps for a fab in their favourite format
* Fab docs for the assembler
* Pick and place files
* PCB 3D model in STEP format

You want to do this in a one-touch way, and make sure everything you need to
do so it securely saved in version control, not on the back of an old
Expand Down Expand Up @@ -144,6 +145,8 @@ The available values for *type* are:
- Bill of Materials
- `kibom` BoM in HTML or CSV format generated by [KiBoM](https://github.com/INTI-CMNB/KiBoM)
- `ibom` Interactive HTML BoM generated by [InteractiveHtmlBom](https://github.com/INTI-CMNB/InteractiveHtmlBom)
- 3D model:
- `step` *Standard for the Exchange of Product Data* for the PCB

Here is an example of a configuration file to generate the gerbers for the top and bottom layers:

Expand Down Expand Up @@ -288,6 +291,13 @@ The valid formats are `hpgl`, `ps`, `gerber`, `dxf`, `svg` and `pdf`. Example:

- `output` filename for the output PDF

### STEP options

- `metric_units` (boolean) use metric units instead of inches.
- `origin` (string) determines the coordinates origin. Using `grid` the coordinates are the same as you have in the design sheet. The `drill` option uses the auxiliar reference defined by the user. You can define any other origin using the format "X,Y", i.e. "3.2,-10".
- `no_virtual` (boolean optional=false) used to exclude 3D models for components with *virtual* attribute.
- `min_distance` (numeric default=0.01 mm) the minimum distance between points to treat them as separate ones.
- `output` (string optional=project.step) name for the generated STEP file.

## Using KiPlot

Expand Down
13 changes: 12 additions & 1 deletion docs/samples/generic_plot.kiplot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,15 @@ outputs:
- layer: F.Cu
suffix: F_Cu
- layer: B.SilkS
suffix: B_Silks
suffix: B_Silks

- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
units: millimeters # millimeters or inches
origin: grid # "grid" or "drill" o "X,Y"
no_virtual: false # exclude 3D models for components with 'virtual' attribute
#min_distance: 0.01 # Minimum distance between points to treat them as separate ones (default 0.01 mm)

22 changes: 20 additions & 2 deletions kiplot/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def _parse_out_opts(self, otype, options, name):
},
{
'key': 'metric_units',
'types': ['excellon'],
'types': ['excellon', 'step'],
'to': 'metric_units',
'required': lambda opts: True,
},
Expand Down Expand Up @@ -409,6 +409,24 @@ def _parse_out_opts(self, otype, options, name):
'to': 'output_name',
'required': lambda opts: True,
},
{
'key': 'origin',
'types': ['step'],
'to': 'origin',
'required': lambda opts: True,
},
{
'key': 'no_virtual',
'types': ['step'],
'to': 'no_virtual',
'required': lambda opts: False,
},
{
'key': 'min_distance',
'types': ['step'],
'to': 'min_distance',
'required': lambda opts: False,
},
]

po = PC.OutputOptions(otype)
Expand Down Expand Up @@ -501,7 +519,7 @@ def _parse_output(self, o_obj):
config_error("Output '"+name+"' needs a type")

if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg',
'gerb_drill', 'excellon', 'position',
'gerb_drill', 'excellon', 'position', 'step',
'kibom', 'ibom', 'pdf_sch_print', 'pdf_pcb_print']:
config_error("Unknown output type '"+otype+"' in '"+name+"'")

Expand Down
45 changes: 45 additions & 0 deletions kiplot/kiplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def plot(self, brd_file, target, invert, skip_pre):
self._do_sch_print(output_dir, op, brd_file)
elif self._output_is_pcb_print(op):
self._do_pcb_print(board, output_dir, op, brd_file)
elif self._output_is_step(op):
self._do_step(output_dir, op, brd_file)
else: # pragma no cover
# We shouldn't get here, means the above if is incomplete
plot_error("Don't know how to plot type "+op.options.type)
Expand Down Expand Up @@ -269,6 +271,9 @@ def _output_is_sch_print(self, output):
def _output_is_pcb_print(self, output):
return output.options.type == PCfg.OutputOptions.PDF_PCB_PRINT

def _output_is_step(self, output):
return output.options.type == PCfg.OutputOptions.STEP

def _output_is_bom(self, output):
return output.options.type in [
PCfg.OutputOptions.KIBOM,
Expand Down Expand Up @@ -602,6 +607,46 @@ def _do_sch_print(self, output_dir, output, brd_file):
logger.debug('Moving '+cur+' -> '+new)
os.rename(cur, new)

def _do_step(self, output_dir, op, brd_file):
to = op.options.type_options
# Output file name
output = to.output
if output is None:
output = os.path.splitext(os.path.basename(brd_file))[0]+'.step'
output = os.path.abspath(os.path.join(output_dir, output))
# Make units explicit
if to.metric_units:
units = 'mm'
else:
units = 'in'
# Base command with overwrite
cmd = [misc.KICAD2STEP, '-o', output, '-f']
# Add user options
if to.no_virtual:
cmd.append('--no-virtual')
if to.min_distance is not None:
cmd.extend(['--min-distance', "{}{}".format(to.min_distance, units)])
if to.origin == 'drill':
cmd.append('--drill-origin')
elif to.origin == 'grid':
cmd.append('--grid-origin')
else:
cmd.extend(['--user-origin', "{}{}".format(to.origin.replace(',', 'x'), units)])
# The board
cmd.append(brd_file)
# Execute and inform is successful
logger.debug('Executing: '+str(cmd))
try:
cmd_output = check_output(cmd, stderr=STDOUT)
except CalledProcessError as e: # pragma: no cover
# Current kicad2step always returns 0!!!!
# This is why I'm excluding it from coverage
logger.error('Failed to create Step file, error %d', e.returncode)
if e.output:
logger.debug('Output from command: '+e.output.decode())
exit(misc.KICAD2STEP_ERR)
logger.debug('Output from command:\n'+cmd_output.decode())

def _do_pcb_print(self, board, output_dir, output, brd_file):
check_script(misc.CMD_PCBNEW_PRINT_LAYERS,
misc.URL_PCBNEW_PRINT_LAYERS, '1.4.1')
Expand Down
2 changes: 2 additions & 0 deletions kiplot/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
NO_YAML_MODULE = 15
NO_PCBNEW_MODULE = 16
CORRUPTED_PCB = 17
KICAD2STEP_ERR = 18

CMD_EESCHEMA_DO = 'eeschema_do'
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts'
Expand All @@ -30,3 +31,4 @@
URL_KIBOM = 'https://github.com/INTI-CMNB/KiBoM'
CMD_IBOM = 'generate_interactive_bom.py'
URL_IBOM = 'https://github.com/INTI-CMNB/InteractiveHtmlBom'
KICAD2STEP = 'kicad2step'
24 changes: 24 additions & 0 deletions kiplot/plot_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pcbnew
import re

from . import error
from . import log
Expand Down Expand Up @@ -377,6 +378,26 @@ def validate(self):
return errs


class StepOptions(TypeOptions):

def __init__(self):
self.metric_units = True
self.origin = None
self.min_distance = None
self.no_virtual = False
self.output = None

def validate(self):
errs = []
# origin (required)
if (self.origin not in ['grid', 'drill']) and (re.match(r'[-\d\.]+\s*,\s*[-\d\.]+\s*$', self.origin) is None):
errs.append('Origin must be "grid" or "drill" or "X,Y"')
# min_distance (not required)
if (self.min_distance is not None) and (not isinstance(self.min_distance, (int, float))):
errs.append('min_distance must be a number')
return errs


class KiBoMOptions(TypeOptions):

def __init__(self):
Expand Down Expand Up @@ -424,6 +445,7 @@ class OutputOptions(object):
IBOM = 'ibom'
PDF_SCH_PRINT = 'pdf_sch_print'
PDF_PCB_PRINT = 'pdf_pcb_print'
STEP = 'step'

def __init__(self, otype):
self.type = otype
Expand Down Expand Up @@ -454,6 +476,8 @@ def __init__(self, otype):
self.type_options = SchPrintOptions()
elif otype == self.PDF_PCB_PRINT:
self.type_options = PcbPrintOptions()
elif otype == self.STEP:
self.type_options = StepOptions()
else: # pragma: no cover
# If we get here it means the above if is incomplete
raise KiPlotConfigurationError("Output options not implemented for "+otype)
Expand Down
2 changes: 1 addition & 1 deletion tests/board_samples/bom.kicad_pcb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
(pad_drill 0.762)
(pad_to_mask_clearance 0.051)
(solder_mask_min_width 0.25)
(aux_axis_origin 0 0)
(aux_axis_origin 148.4 80.2)
(visible_elements FFFFFF7F)
(pcbplotparams
(layerselection 0x010fc_ffffffff)
Expand Down
9 changes: 6 additions & 3 deletions tests/test_plot/test_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
import os
import sys
# Look for the 'utils' module from where the script is running
prev_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(prev_dir)
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
from kiplot.misc import (BOM_ERROR)

BOM_DIR = 'BoM'
Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_drill.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_dxf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_hpgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
9 changes: 6 additions & 3 deletions tests/test_plot/test_ibom.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
import os
import sys
# Look for the 'utils' module from where the script is running
prev_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(prev_dir)
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
from kiplot.misc import (BOM_ERROR)

BOM_DIR = 'BoM'
Expand Down
9 changes: 6 additions & 3 deletions tests/test_plot/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
import sys
import shutil
# Look for the 'utils' module from where the script is running
prev_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context
sys.path.insert(0, os.path.dirname(prev_dir))
prev_dir = os.path.dirname(prev_dir)
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
from kiplot.misc import (EXIT_BAD_ARGS, EXIT_BAD_CONFIG, NO_SCH_FILE, NO_PCB_FILE)


Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
7 changes: 4 additions & 3 deletions tests/test_plot/test_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
import sys
import logging
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down Expand Up @@ -58,7 +59,7 @@ def test_update_xml():
try:
ctx.run()
# Check all outputs are there
ctx.expect_out_file(prj+'.csv')
# ctx.expect_out_file(prj+'.csv')
assert os.path.isfile(xml)
assert os.path.getsize(xml) > 0
logging.debug(os.path.basename(xml)+' OK')
Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_print_pcb.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
5 changes: 3 additions & 2 deletions tests/test_plot/test_print_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context

Expand Down
Loading

0 comments on commit 7136662

Please sign in to comment.