Merge pull request #2 from INTI-CMNB/step_output

3D STEP output added
This commit is contained in:
Salvador E. Tropea 2020-06-15 16:36:39 -03:00 committed by GitHub
commit 7136662cb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 303 additions and 36 deletions

View File

@ -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

View File

@ -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
@ -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:
@ -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

View File

@ -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)

View File

@ -339,7 +339,7 @@ class CfgYamlReader(CfgReader):
},
{
'key': 'metric_units',
'types': ['excellon'],
'types': ['excellon', 'step'],
'to': 'metric_units',
'required': lambda opts: True,
},
@ -409,6 +409,24 @@ class CfgYamlReader(CfgReader):
'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)
@ -501,7 +519,7 @@ class CfgYamlReader(CfgReader):
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+"'")

View File

@ -123,6 +123,8 @@ class Plotter(object):
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)
@ -269,6 +271,9 @@ class Plotter(object):
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,
@ -602,6 +607,46 @@ class Plotter(object):
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')

View File

@ -19,6 +19,7 @@ PLOT_ERROR = 14
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'
@ -30,3 +31,4 @@ CMD_KIBOM = 'KiBOM_CLI.py'
URL_KIBOM = 'https://github.com/INTI-CMNB/KiBoM'
CMD_IBOM = 'generate_interactive_bom.py'
URL_IBOM = 'https://github.com/INTI-CMNB/InteractiveHtmlBom'
KICAD2STEP = 'kicad2step'

View File

@ -1,4 +1,5 @@
import pcbnew
import re
from . import error
from . import log
@ -377,6 +378,26 @@ class PositionOptions(TypeOptions):
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):
@ -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
@ -454,6 +476,8 @@ class OutputOptions(object):
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)

View File

@ -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)

View File

@ -13,11 +13,14 @@ pytest-3 --log-cli-level debug
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'

View File

@ -13,8 +13,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -8,8 +8,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -8,8 +8,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -13,11 +13,14 @@ pytest-3 --log-cli-level debug
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'

View File

@ -24,11 +24,14 @@ import os
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)

View File

@ -8,8 +8,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -21,8 +21,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -15,8 +15,9 @@ import os
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
@ -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')

View File

@ -12,8 +12,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -12,8 +12,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -8,8 +8,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -9,8 +9,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -0,0 +1,49 @@
"""
Tests of Printing Schematic files
We test:
- STEP for bom.kicad_pcb
For debug information use:
pytest-3 --log-cli-level debug
"""
import os
import sys
# Look for the 'utils' module from where the script is running
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
STEP_DIR = '3D'
# STEP_FILE = 'bom.step'
def test_step_1():
prj = 'bom'
ctx = context.TestContext('STEP_1', prj, 'step_simple', STEP_DIR)
ctx.run()
# Check all outputs are there
ctx.expect_out_file(os.path.join(STEP_DIR, prj+'.step'))
ctx.clean_up()
def test_step_2():
prj = 'bom'
ctx = context.TestContext('STEP_2', prj, 'step_simple_2', STEP_DIR)
ctx.run()
# Check all outputs are there
ctx.expect_out_file(os.path.join(STEP_DIR, prj+'.step'))
ctx.clean_up()
def test_step_3():
prj = 'bom'
ctx = context.TestContext('STEP_3', prj, 'step_simple_3', STEP_DIR)
ctx.run()
# Check all outputs are there
ctx.expect_out_file(os.path.join(STEP_DIR, prj+'.step'))
ctx.clean_up()

View File

@ -8,8 +8,9 @@ pytest-3 --log-cli-level debug
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

View File

@ -5,6 +5,8 @@ Tests various errors in the config file
- Wrong kiplot.version
- Missing drill map type
- Wrong drill map type
- Wrong step origin
- Wrong step min_distance
- Wrong layer:
- Incorrect name
- Inner.1, but no inner layers
@ -105,3 +107,17 @@ def test_no_layers():
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("You must specify the layers for `PDF`")
ctx.clean_up()
def test_error_step_origin():
ctx = context.TestContext('ErrorStepOrigin', 'bom', 'error_step_origin', None)
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Origin must be")
ctx.clean_up()
def test_error_step_min_distance():
ctx = context.TestContext('ErrorStepMinDistance', 'bom', 'error_step_min_distance', None)
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("min_distance must be a number")
ctx.clean_up()

View File

@ -0,0 +1,14 @@
kiplot:
version: 1
outputs:
- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
metric_units: false
origin: "3.2 , -10" # "grid" or "drill" o "X,Y" i.e. 3.2,-10
#no_virtual: false # exclude 3D models for components with 'virtual' attribute
min_distance: bogus # Minimum distance between points to treat them as separate ones (default 0.01 mm)
#output: project.step

View File

@ -0,0 +1,14 @@
kiplot:
version: 1
outputs:
- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
metric_units: true
origin: bogus # "grid" or "drill" o "X,Y" i.e. 3.2,-10
no_virtual: true # 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)
#output: project.step

View File

@ -0,0 +1,14 @@
kiplot:
version: 1
outputs:
- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
metric_units: true
origin: drill # "grid" or "drill" o "X,Y" i.e. 3.2,-10
#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)
#output: project.step

View File

@ -0,0 +1,14 @@
kiplot:
version: 1
outputs:
- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
metric_units: false
origin: "3.2,-10" # "grid" or "drill" o "X,Y" i.e. 3.2,-10
no_virtual: true # exclude 3D models for components with 'virtual' attribute
min_distance: 0.0004 # Minimum distance between points to treat them as separate ones (default 0.01 mm)
#output: project.step

View File

@ -0,0 +1,14 @@
kiplot:
version: 1
outputs:
- name: Step
comment: "Generate 3D model (STEP)"
type: step
dir: 3D
options:
metric_units: false
origin: grid # "grid" or "drill" o "X,Y" i.e. 3.2,-10
no_virtual: true # exclude 3D models for components with 'virtual' attribute
min_distance: 0.0004 # Minimum distance between points to treat them as separate ones (default 0.01 mm)
#output: project.step