parent
b4c1531e10
commit
27b26feb88
|
|
@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Default global `dir` option.
|
||||
- Pattern to expand the variant name: %V
|
||||
- PCB PDF Print: mechanism to change the block title. (#102)
|
||||
- 3D view render
|
||||
|
||||
### Changed
|
||||
- Internal BoM: now components with different Tolerance, Voltage, Current
|
||||
|
|
|
|||
37
README.md
37
README.md
|
|
@ -58,6 +58,7 @@ For example, it's common that you might want for each board rev:
|
|||
* Fab docs for the assembler, including the BoM (Bill of Materials), costs spreadsheet and board view
|
||||
* Pick and place files
|
||||
* PCB 3D model in STEP format
|
||||
* PCB 3D render in PNG 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
|
||||
|
|
@ -1396,6 +1397,42 @@ Next time you need this list just use an alias, like this:
|
|||
- `width_adjust`: [number=0] This width factor is intended to compensate PS printers/plotters that do not strictly obey line width settings.
|
||||
Only used to plot pads and tracks.
|
||||
|
||||
* 3D render of the PCB
|
||||
* Type: `render_3d`
|
||||
* Description: Exports the image generated by KiCad's 3D viewer.
|
||||
* Valid keys:
|
||||
- `comment`: [string=''] A comment for documentation purposes.
|
||||
- `dir`: [string='./'] Output directory for the generated files. If it starts with `+` the rest is concatenated to the default dir.
|
||||
- `name`: [string=''] Used to identify this particular output definition.
|
||||
- `options`: [dict] Options for the `render_3d` output.
|
||||
* Valid keys:
|
||||
- `background1`: [string='#66667F'] First color for the background gradient.
|
||||
- `background2`: [string='#CCCCE5'] Second color for the background gradient.
|
||||
- `board`: [string='#332B16'] Color for the board without copper or solder mask.
|
||||
- `copper`: [string='#B29C00'] Color for the copper.
|
||||
- `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted.
|
||||
A short-cut to use for simple cases where a variant is an overkill.
|
||||
- `download`: [boolean=true] Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD.
|
||||
- `kicad_3d_url`: [string='https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'] Base URL for the KiCad 3D models.
|
||||
- `move_x`: [number=0] Steps to move in the X axis, positive is to the right.
|
||||
Just like pressing the right arrow in the 3D viewer.
|
||||
- `move_y`: [number=0] Steps to move in the Y axis, positive is up.
|
||||
Just like pressing the up arrow in the 3D viewer.
|
||||
- `no_smd`: [boolean=false] Used to exclude 3D models for surface mount components.
|
||||
- `no_tht`: [boolean=false] Used to exclude 3D models for through hole components.
|
||||
- `no_virtual`: [boolean=false] Used to exclude 3D models for components with 'virtual' attribute.
|
||||
- `output`: [string='%f-%i%v.%x'] Name for the generated image file (%i='3D_$VIEW' %x='png'). Affected by global options.
|
||||
- `ray_tracing`: [boolean=false] Enable the ray tracing. Much better result, but slow, and you'll need to adjust `wait_rt`.
|
||||
- `silk`: [string='#E5E5E5'] Color for the silk screen.
|
||||
- `solder_mask`: [string='#143324'] Color for the solder mask.
|
||||
- `solder_paste`: [string='#808080'] Color for the solder paste.
|
||||
- `variant`: [string=''] Board variant to apply.
|
||||
- `view`: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
- `wait_ray_tracing`: [number=5] How many seconds we must wait before capturing the ray tracing render.
|
||||
Lamentably KiCad can save an unfinished image. Enlarge it if your image looks partially rendered.
|
||||
- `zoom`: [number=0] Zoom steps. Use positive to enlarge, get closer, and negative to reduce.
|
||||
Same result as using the mouse wheel in the 3D viewer.
|
||||
|
||||
* Schematic with variant generator
|
||||
* Type: `sch_variant`
|
||||
* Description: Creates a copy of the schematic with all the filters and variants applied.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ For example, it's common that you might want for each board rev:
|
|||
* Fab docs for the assembler, including the BoM (Bill of Materials), costs spreadsheet and board view
|
||||
* Pick and place files
|
||||
* PCB 3D model in STEP format
|
||||
* PCB 3D render in PNG 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
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,60 @@ outputs:
|
|||
width_adjust: 0
|
||||
layers: all
|
||||
|
||||
# 3D render of the PCB:
|
||||
- name: 'render_3d_example'
|
||||
comment: 'Exports the image generated by KiCad's 3D viewer.'
|
||||
type: 'render_3d'
|
||||
dir: 'Example/render_3d_dir'
|
||||
options:
|
||||
# [string='#66667F'] First color for the background gradient
|
||||
background1: '#66667F'
|
||||
# [string='#CCCCE5'] Second color for the background gradient
|
||||
background2: '#CCCCE5'
|
||||
# [string='#332B16'] Color for the board without copper or solder mask
|
||||
board: '#332B16'
|
||||
# [string='#B29C00'] Color for the copper
|
||||
copper: '#B29C00'
|
||||
# [string|list(string)=''] Name of the filter to mark components as not fitted.
|
||||
# A short-cut to use for simple cases where a variant is an overkill
|
||||
dnf_filter: ''
|
||||
# [boolean=true] Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD
|
||||
download: true
|
||||
# [string='https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'] Base URL for the KiCad 3D models
|
||||
kicad_3d_url: 'https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'
|
||||
# [number=0] Steps to move in the X axis, positive is to the right.
|
||||
# Just like pressing the right arrow in the 3D viewer
|
||||
move_x: 0
|
||||
# [number=0] Steps to move in the Y axis, positive is up.
|
||||
# Just like pressing the up arrow in the 3D viewer
|
||||
move_y: 0
|
||||
# [boolean=false] Used to exclude 3D models for surface mount components
|
||||
no_smd: false
|
||||
# [boolean=false] Used to exclude 3D models for through hole components
|
||||
no_tht: false
|
||||
# [boolean=false] Used to exclude 3D models for components with 'virtual' attribute
|
||||
no_virtual: false
|
||||
# [string='%f-%i%v.%x'] Name for the generated image file (%i='3D_$VIEW' %x='png'). Affected by global options
|
||||
output: '%f-%i%v.%x'
|
||||
# [boolean=false] Enable the ray tracing. Much better result, but slow, and you'll need to adjust `wait_rt`
|
||||
ray_tracing: false
|
||||
# [string='#E5E5E5'] Color for the silk screen
|
||||
silk: '#E5E5E5'
|
||||
# [string='#143324'] Color for the solder mask
|
||||
solder_mask: '#143324'
|
||||
# [string='#808080'] Color for the solder paste
|
||||
solder_paste: '#808080'
|
||||
# [string=''] Board variant to apply
|
||||
variant: ''
|
||||
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view
|
||||
view: 'top'
|
||||
# [number=5] How many seconds we must wait before capturing the ray tracing render.
|
||||
# Lamentably KiCad can save an unfinished image. Enlarge it if your image looks partially rendered
|
||||
wait_ray_tracing: 5
|
||||
# [number=0] Zoom steps. Use positive to enlarge, get closer, and negative to reduce.
|
||||
# Same result as using the mouse wheel in the 3D viewer
|
||||
zoom: 0
|
||||
|
||||
# Schematic with variant generator:
|
||||
# This copy isn't intended for development.
|
||||
# Is just a tweaked version of the original where you can look at the results.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ PCBDRAW_ERR = 20
|
|||
SVG_SCH_PRINT = 21
|
||||
CORRUPTED_SCH = 22
|
||||
WRONG_INSTALL = 23
|
||||
RENDER_3D_ERR = 24
|
||||
error_level_to_name = ['NONE',
|
||||
'INTERNAL_ERROR',
|
||||
'WRONG_ARGUMENTS',
|
||||
|
|
@ -58,13 +59,16 @@ error_level_to_name = ['NONE',
|
|||
'SVG_SCH_PRINT',
|
||||
'CORRUPTED_SCH',
|
||||
'WRONG_INSTALL',
|
||||
'RENDER_3D_ERR',
|
||||
]
|
||||
CMD_EESCHEMA_DO = 'eeschema_do'
|
||||
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts'
|
||||
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/KiAuto'
|
||||
CMD_PCBNEW_RUN_DRC = 'pcbnew_do'
|
||||
URL_PCBNEW_RUN_DRC = URL_EESCHEMA_DO
|
||||
CMD_PCBNEW_PRINT_LAYERS = 'pcbnew_do'
|
||||
CMD_PCBNEW_PRINT_LAYERS = CMD_PCBNEW_RUN_DRC
|
||||
URL_PCBNEW_PRINT_LAYERS = URL_EESCHEMA_DO
|
||||
CMD_PCBNEW_3D = CMD_PCBNEW_RUN_DRC
|
||||
URL_PCBNEW_3D = URL_EESCHEMA_DO
|
||||
CMD_KIBOM = 'KiBOM_CLI.py'
|
||||
URL_KIBOM = 'https://github.com/INTI-CMNB/KiBoM'
|
||||
CMD_IBOM = 'generate_interactive_bom.py'
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Optionable(object):
|
|||
_str_values_re = compile(r"string=.*\] \[([^\]]+)\]")
|
||||
_num_range_re = compile(r"number=.*\] \[(-?\d+),(-?\d+)\]")
|
||||
_default = None
|
||||
_color_re = re.compile(r"#[A-Fa-f0-9]{6}$")
|
||||
|
||||
def __init__(self):
|
||||
self._unkown_is_error = False
|
||||
|
|
@ -267,6 +268,15 @@ class Optionable(object):
|
|||
def get_default(cls):
|
||||
return cls._default
|
||||
|
||||
def validate_color(self, name):
|
||||
color = getattr(self, name)
|
||||
if not self._color_re.match(color):
|
||||
raise KiPlotConfigurationError('Invalid color for `{}` use `#rrggbb` with hex digits'.format(name))
|
||||
|
||||
def validate_colors(self, names):
|
||||
for color in names:
|
||||
self.validate_color(color)
|
||||
|
||||
|
||||
class BaseOptions(Optionable):
|
||||
""" A class to validate and hold output options.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020-2021 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
import os
|
||||
import requests
|
||||
import tempfile
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .error import KiPlotConfigurationError
|
||||
from .misc import W_MISS3D, W_FAILDL
|
||||
from .gs import (GS)
|
||||
from .out_base import VariantOptions, BaseOutput
|
||||
from .kicad.config import KiConf
|
||||
from .macros import macros, document # noqa: F401
|
||||
from . import log
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
|
||||
|
||||
class Base3DOptions(VariantOptions):
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.no_virtual = False
|
||||
""" Used to exclude 3D models for components with 'virtual' attribute """
|
||||
self.download = True
|
||||
""" Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD """
|
||||
self.kicad_3d_url = 'https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'
|
||||
""" Base URL for the KiCad 3D models """
|
||||
# Temporal dir used to store the downloaded files
|
||||
self._tmp_dir = None
|
||||
super().__init__()
|
||||
self._expand_id = '3D'
|
||||
|
||||
def download_model(self, url, fname):
|
||||
""" Download the 3D model from the provided URL """
|
||||
logger.debug('Downloading `{}`'.format(url))
|
||||
r = requests.get(url, allow_redirects=True)
|
||||
if r.status_code != 200:
|
||||
logger.warning(W_FAILDL+'Failed to download `{}`'.format(url))
|
||||
return None
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = tempfile.mkdtemp()
|
||||
logger.debug('Using `{}` as temporal dir for downloaded files'.format(self._tmp_dir))
|
||||
dest = os.path.join(self._tmp_dir, fname)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
with open(dest, 'wb') as f:
|
||||
f.write(r.content)
|
||||
return dest
|
||||
|
||||
def undo_3d_models_rename(self):
|
||||
""" Restores the file name for any renamed 3D module """
|
||||
for m in GS.board.GetModules():
|
||||
# Get the model references
|
||||
models = m.Models()
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Fix any changed path
|
||||
replaced = self.undo_3d_models_rep.get(m.GetReference())
|
||||
for i, m3d in enumerate(models_l):
|
||||
if m3d.m_Filename in self.undo_3d_models:
|
||||
m3d.m_Filename = self.undo_3d_models[m3d.m_Filename]
|
||||
if replaced:
|
||||
m3d.m_Filename = replaced[i]
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
|
||||
def replace_models(self, models, new_model, c):
|
||||
""" Changes the 3D model using a provided model """
|
||||
logger.debug('Changing 3D models for '+c.ref)
|
||||
# Get the model references
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Check if we have more than one model
|
||||
c_models = len(models_l)
|
||||
if c_models > 1:
|
||||
new_model = new_model.split(',')
|
||||
c_replace = len(new_model)
|
||||
if c_models != c_replace:
|
||||
raise KiPlotConfigurationError('Found {} models in component {}, but {} replacements provided'.
|
||||
format(c_models, c, c_replace))
|
||||
else:
|
||||
new_model = [new_model]
|
||||
# Change the models
|
||||
replaced = []
|
||||
for i, m3d in enumerate(models_l):
|
||||
replaced.append(m3d.m_Filename)
|
||||
m3d.m_Filename = new_model[i]
|
||||
self.undo_3d_models_rep[c.ref] = replaced
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
|
||||
def download_models(self):
|
||||
""" Check we have the 3D models.
|
||||
Inform missing models.
|
||||
Try to download the missing models """
|
||||
models_replaced = False
|
||||
# Load KiCad configuration so we can expand the 3D models path
|
||||
KiConf.init(GS.pcb_file)
|
||||
# List of models we already downloaded
|
||||
downloaded = set()
|
||||
self.undo_3d_models = {}
|
||||
# Look for all the footprints
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
# Extract the models (the iterator returns copies)
|
||||
models = m.Models()
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Look for all the 3D models for this footprint
|
||||
for m3d in models_l:
|
||||
full_name = KiConf.expand_env(m3d.m_Filename)
|
||||
if not os.path.isfile(full_name):
|
||||
# Missing 3D model
|
||||
if full_name not in downloaded:
|
||||
logger.warning(W_MISS3D+'Missing 3D model for {}: `{}`'.format(ref, full_name))
|
||||
if self.download and m3d.m_Filename.startswith('${KISYS3DMOD}/'):
|
||||
# This is a model from KiCad, try to download it
|
||||
fname = m3d.m_Filename[14:]
|
||||
replace = None
|
||||
if full_name in downloaded:
|
||||
# Already downloaded
|
||||
replace = os.path.join(self._tmp_dir, fname)
|
||||
else:
|
||||
# Download the model
|
||||
url = self.kicad_3d_url+fname
|
||||
replace = self.download_model(url, fname)
|
||||
if replace:
|
||||
# Successfully downloaded
|
||||
downloaded.add(full_name)
|
||||
self.undo_3d_models[replace] = m3d.m_Filename
|
||||
# If this is a .wrl also download the .step
|
||||
if url.endswith('.wrl'):
|
||||
url = url[:-4]+'.step'
|
||||
fname = fname[:-4]+'.step'
|
||||
self.download_model(url, fname)
|
||||
if replace:
|
||||
m3d.m_Filename = replace
|
||||
models_replaced = True
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
return models_replaced
|
||||
|
||||
def list_models(self):
|
||||
""" Get the list of 3D models """
|
||||
# Load KiCad configuration so we can expand the 3D models path
|
||||
KiConf.init(GS.pcb_file)
|
||||
models = set()
|
||||
# Look for all the footprints
|
||||
for m in GS.board.GetModules():
|
||||
# Look for all the 3D models for this footprint
|
||||
for m3d in m.Models():
|
||||
full_name = KiConf.expand_env(m3d.m_Filename)
|
||||
if os.path.isfile(full_name):
|
||||
models.add(full_name)
|
||||
return list(models)
|
||||
|
||||
def save_board(self, dir):
|
||||
""" Save the PCB to a temporal file """
|
||||
with NamedTemporaryFile(mode='w', suffix='.kicad_pcb', delete=False, dir=dir) as f:
|
||||
fname = f.name
|
||||
logger.debug('Storing modified PCB to `{}`'.format(fname))
|
||||
GS.board.Save(fname)
|
||||
return fname
|
||||
|
||||
def filter_components(self, dir):
|
||||
self.undo_3d_models_rep = {}
|
||||
if not self._comps:
|
||||
# No variant/filter to apply
|
||||
if self.download_models():
|
||||
# Some missing components found and we downloaded them
|
||||
# Save the fixed board
|
||||
ret = self.save_board(dir)
|
||||
# Undo the changes
|
||||
self.undo_3d_models_rename()
|
||||
return ret
|
||||
return GS.pcb_file
|
||||
comps_hash = self.get_refs_hash()
|
||||
# Remove the 3D models for not fitted components
|
||||
rem_models = []
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
c = comps_hash.get(ref, None)
|
||||
if c:
|
||||
# The filter/variant knows about this component
|
||||
models = m.Models()
|
||||
if c.included and not c.fitted:
|
||||
# Not fitted, remove the 3D model
|
||||
rem_m_models = []
|
||||
while not models.empty():
|
||||
rem_m_models.append(models.pop())
|
||||
rem_models.append(rem_m_models)
|
||||
else:
|
||||
# Fitted
|
||||
new_model = c.get_field_value(GS.global_3D_model_field)
|
||||
if new_model:
|
||||
# We will change the 3D model
|
||||
self.replace_models(models, new_model, c)
|
||||
self.download_models()
|
||||
fname = self.save_board(dir)
|
||||
self.undo_3d_models_rename()
|
||||
# Undo the removing
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
c = comps_hash.get(ref, None)
|
||||
if c and c.included and not c.fitted:
|
||||
models = m.Models()
|
||||
restore = rem_models.pop(0)
|
||||
for model in restore:
|
||||
models.push_front(model)
|
||||
return fname
|
||||
|
||||
def get_targets(self, out_dir):
|
||||
return [self._parent.expand_filename(out_dir, self.output)]
|
||||
|
||||
|
||||
class Base3D(BaseOutput):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_dependencies(self):
|
||||
files = super().get_dependencies()
|
||||
files.extend(self.options.list_models())
|
||||
return files
|
||||
|
|
@ -4,14 +4,12 @@
|
|||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
import os
|
||||
import re
|
||||
from tempfile import (NamedTemporaryFile)
|
||||
# Here we import the whole module to make monkeypatch work
|
||||
import subprocess
|
||||
import shutil
|
||||
from .misc import PCBDRAW, PCBDRAW_ERR, URL_PCBDRAW, W_AMBLIST, W_UNRETOOL, W_USESVG2, W_USEIMAGICK
|
||||
from .kiplot import check_script
|
||||
from .error import KiPlotConfigurationError
|
||||
from .gs import (GS)
|
||||
from .optionable import Optionable
|
||||
from .out_base import VariantOptions
|
||||
|
|
@ -24,8 +22,6 @@ CONVERT = 'convert'
|
|||
|
||||
|
||||
class PcbDrawStyle(Optionable):
|
||||
_color_re = re.compile(r"#[A-Fa-f0-9]{6}$")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with document:
|
||||
|
|
@ -50,21 +46,9 @@ class PcbDrawStyle(Optionable):
|
|||
self.highlight_padding = 1.5
|
||||
""" [0,1000] how much the highlight extends around the component [mm] """
|
||||
|
||||
def validate_color(self, name):
|
||||
color = getattr(self, name)
|
||||
if not self._color_re.match(color):
|
||||
raise KiPlotConfigurationError('Invalid color for `{}` use `#rrggbb` with hex digits'.format(name))
|
||||
|
||||
def config(self, parent):
|
||||
super().config(parent)
|
||||
self.validate_color('board')
|
||||
self.validate_color('copper')
|
||||
self.validate_color('board')
|
||||
self.validate_color('silk')
|
||||
self.validate_color('pads')
|
||||
self.validate_color('outline')
|
||||
self.validate_color('clad')
|
||||
self.validate_color('vcut')
|
||||
self.validate_colors(['board', 'copper', 'board', 'silk', 'pads', 'outline', 'clad', 'vcut'])
|
||||
# Not implemented but required
|
||||
self.highlight_offset = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021 Salvador E. Tropea
|
||||
# Copyright (c) 2021 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
import os
|
||||
from shutil import rmtree
|
||||
from .misc import CMD_PCBNEW_3D, URL_PCBNEW_3D, RENDER_3D_ERR
|
||||
from .gs import (GS)
|
||||
from .kiplot import check_script, exec_with_retry, add_extra_options
|
||||
from .out_base_3d import Base3DOptions, Base3D
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from . import log
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
|
||||
|
||||
class Render3DOptions(Base3DOptions):
|
||||
_colors = {'background1': 'bg_color_1',
|
||||
'background2': 'bg_color_2',
|
||||
'copper': 'copper_color',
|
||||
'board': 'board_color',
|
||||
'silk': 'silk_color',
|
||||
'solder_mask': 'sm_color',
|
||||
'solder_paste': 'sp_color'}
|
||||
_views = {'top': 'z', 'bottom': 'Z', 'front': 'y', 'rear': 'Y', 'right': 'x', 'left': 'X'}
|
||||
_rviews = {v: k for k, v in _views.items()}
|
||||
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.output = GS.def_global_output
|
||||
""" Name for the generated image file (%i='3D_$VIEW' %x='png') """
|
||||
self.no_tht = False
|
||||
""" Used to exclude 3D models for through hole components """
|
||||
self.no_smd = False
|
||||
""" Used to exclude 3D models for surface mount components """
|
||||
self.background1 = "#66667F"
|
||||
""" First color for the background gradient """
|
||||
self.background2 = "#CCCCE5"
|
||||
""" Second color for the background gradient """
|
||||
self.board = "#332B16"
|
||||
""" Color for the board without copper or solder mask """
|
||||
self.copper = "#B29C00"
|
||||
""" Color for the copper """
|
||||
self.silk = "#E5E5E5"
|
||||
""" Color for the silk screen """
|
||||
self.solder_mask = "#143324"
|
||||
""" Color for the solder mask """
|
||||
self.solder_paste = "#808080"
|
||||
""" Color for the solder paste """
|
||||
self.move_x = 0
|
||||
""" Steps to move in the X axis, positive is to the right.
|
||||
Just like pressing the right arrow in the 3D viewer """
|
||||
self.move_y = 0
|
||||
""" Steps to move in the Y axis, positive is up.
|
||||
Just like pressing the up arrow in the 3D viewer """
|
||||
self.ray_tracing = False
|
||||
""" Enable the ray tracing. Much better result, but slow, and you'll need to adjust `wait_rt` """
|
||||
self.wait_ray_tracing = 5
|
||||
""" How many seconds we must wait before capturing the ray tracing render.
|
||||
Lamentably KiCad can save an unfinished image. Enlarge it if your image looks partially rendered """
|
||||
self.view = 'top'
|
||||
""" [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view """
|
||||
self.zoom = 0
|
||||
""" Zoom steps. Use positive to enlarge, get closer, and negative to reduce.
|
||||
Same result as using the mouse wheel in the 3D viewer """
|
||||
super().__init__()
|
||||
self._expand_ext = 'png'
|
||||
|
||||
def config(self, parent):
|
||||
super().config(parent)
|
||||
self.validate_colors(self._colors.keys())
|
||||
view = self._views.get(self.view, None)
|
||||
if view is not None:
|
||||
self.view = view
|
||||
self._expand_id += '_'+self._rviews.get(self.view)
|
||||
|
||||
def run(self, output):
|
||||
super().run(output)
|
||||
check_script(CMD_PCBNEW_3D, URL_PCBNEW_3D, '1.5.11')
|
||||
# Base command with overwrite
|
||||
cmd = [CMD_PCBNEW_3D, '3d_view', '--output_name', output]
|
||||
# Add user options
|
||||
if not self.no_virtual:
|
||||
cmd.append('--virtual')
|
||||
if self.no_tht:
|
||||
cmd.append('--no_tht')
|
||||
if self.no_smd:
|
||||
cmd.append('--no_smd')
|
||||
for color, option in self._colors.items():
|
||||
cmd.extend(['--'+option, getattr(self, color)])
|
||||
if self.move_x:
|
||||
cmd.extend(['--move_x', str(self.move_x)])
|
||||
if self.move_y:
|
||||
cmd.extend(['--move_y', str(self.move_y)])
|
||||
if self.zoom:
|
||||
cmd.extend(['--zoom', str(self.zoom)])
|
||||
if self.wait_ray_tracing != 5:
|
||||
cmd.extend(['--wait_rt', str(self.wait_ray_tracing)])
|
||||
if self.ray_tracing:
|
||||
cmd.append('--ray_tracing')
|
||||
if self.view != 'z':
|
||||
cmd.extend(['--view', self.view])
|
||||
# The board
|
||||
board_name = self.filter_components(GS.pcb_dir)
|
||||
cmd.extend([board_name, os.path.dirname(output)])
|
||||
cmd, video_remove = add_extra_options(cmd)
|
||||
# Execute it
|
||||
ret = exec_with_retry(cmd)
|
||||
# Remove the temporal PCB
|
||||
if board_name != GS.pcb_file:
|
||||
os.remove(board_name)
|
||||
# Remove the downloaded 3D models
|
||||
if self._tmp_dir:
|
||||
rmtree(self._tmp_dir)
|
||||
if ret:
|
||||
logger.error(CMD_PCBNEW_3D+' returned %d', ret)
|
||||
exit(RENDER_3D_ERR)
|
||||
if video_remove:
|
||||
video_name = os.path.join(GS.out_dir, 'pcbnew_3d_view_screencast.ogv')
|
||||
if os.path.isfile(video_name):
|
||||
os.remove(video_name)
|
||||
|
||||
|
||||
@output_class
|
||||
class Render_3D(Base3D): # noqa: F821
|
||||
""" 3D render of the PCB
|
||||
Exports the image generated by KiCad's 3D viewer. """
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with document:
|
||||
self.options = Render3DOptions
|
||||
""" [dict] Options for the `render_3d` output """
|
||||
|
|
@ -5,23 +5,19 @@
|
|||
# Project: KiBot (formerly KiPlot)
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import tempfile
|
||||
from subprocess import (check_output, STDOUT, CalledProcessError)
|
||||
from tempfile import NamedTemporaryFile
|
||||
from shutil import rmtree
|
||||
from .error import KiPlotConfigurationError
|
||||
from .misc import KICAD2STEP, KICAD2STEP_ERR, W_MISS3D, W_FAILDL
|
||||
from .misc import KICAD2STEP, KICAD2STEP_ERR
|
||||
from .gs import (GS)
|
||||
from .out_base import VariantOptions
|
||||
from .kicad.config import KiConf
|
||||
from .out_base_3d import Base3DOptions, Base3D
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from . import log
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
|
||||
|
||||
class STEPOptions(VariantOptions):
|
||||
class STEPOptions(Base3DOptions):
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.metric_units = True
|
||||
|
|
@ -30,20 +26,13 @@ class STEPOptions(VariantOptions):
|
|||
""" Determines the coordinates origin. Using grid the coordinates are the same as you have in the design sheet.
|
||||
The drill option uses the auxiliary reference defined by the user.
|
||||
You can define any other origin using the format 'X,Y', i.e. '3.2,-10' """
|
||||
self.no_virtual = False
|
||||
""" Used to exclude 3D models for components with 'virtual' attribute """
|
||||
self.min_distance = -1
|
||||
""" The minimum distance between points to treat them as separate ones (-1 is KiCad default: 0.01 mm) """
|
||||
self.output = GS.def_global_output
|
||||
""" Name for the generated STEP file (%i='3D' %x='step') """
|
||||
self.download = True
|
||||
""" Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD """
|
||||
self.kicad_3d_url = 'https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'
|
||||
""" Base URL for the KiCad 3D models """
|
||||
# Temporal dir used to store the downloaded files
|
||||
self._tmp_dir = None
|
||||
super().__init__()
|
||||
self._expand_id = '3D'
|
||||
self._expand_ext = 'step'
|
||||
|
||||
@property
|
||||
|
|
@ -56,193 +45,6 @@ class STEPOptions(VariantOptions):
|
|||
raise KiPlotConfigurationError('Origin must be `grid` or `drill` or `X,Y`')
|
||||
self._origin = val
|
||||
|
||||
def download_model(self, url, fname):
|
||||
""" Download the 3D model from the provided URL """
|
||||
logger.debug('Downloading `{}`'.format(url))
|
||||
r = requests.get(url, allow_redirects=True)
|
||||
if r.status_code != 200:
|
||||
logger.warning(W_FAILDL+'Failed to download `{}`'.format(url))
|
||||
return None
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = tempfile.mkdtemp()
|
||||
logger.debug('Using `{}` as temporal dir for downloaded files'.format(self._tmp_dir))
|
||||
dest = os.path.join(self._tmp_dir, fname)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
with open(dest, 'wb') as f:
|
||||
f.write(r.content)
|
||||
return dest
|
||||
|
||||
def undo_3d_models_rename(self):
|
||||
""" Restores the file name for any renamed 3D module """
|
||||
for m in GS.board.GetModules():
|
||||
# Get the model references
|
||||
models = m.Models()
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Fix any changed path
|
||||
replaced = self.undo_3d_models_rep.get(m.GetReference())
|
||||
for i, m3d in enumerate(models_l):
|
||||
if m3d.m_Filename in self.undo_3d_models:
|
||||
m3d.m_Filename = self.undo_3d_models[m3d.m_Filename]
|
||||
if replaced:
|
||||
m3d.m_Filename = replaced[i]
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
|
||||
def replace_models(self, models, new_model, c):
|
||||
""" Changes the 3D model using a provided model """
|
||||
logger.debug('Changing 3D models for '+c.ref)
|
||||
# Get the model references
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Check if we have more than one model
|
||||
c_models = len(models_l)
|
||||
if c_models > 1:
|
||||
new_model = new_model.split(',')
|
||||
c_replace = len(new_model)
|
||||
if c_models != c_replace:
|
||||
raise KiPlotConfigurationError('Found {} models in component {}, but {} replacements provided'.
|
||||
format(c_models, c, c_replace))
|
||||
else:
|
||||
new_model = [new_model]
|
||||
# Change the models
|
||||
replaced = []
|
||||
for i, m3d in enumerate(models_l):
|
||||
replaced.append(m3d.m_Filename)
|
||||
m3d.m_Filename = new_model[i]
|
||||
self.undo_3d_models_rep[c.ref] = replaced
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
|
||||
def download_models(self):
|
||||
""" Check we have the 3D models.
|
||||
Inform missing models.
|
||||
Try to download the missing models """
|
||||
models_replaced = False
|
||||
# Load KiCad configuration so we can expand the 3D models path
|
||||
KiConf.init(GS.pcb_file)
|
||||
# List of models we already downloaded
|
||||
downloaded = set()
|
||||
self.undo_3d_models = {}
|
||||
# Look for all the footprints
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
# Extract the models (the iterator returns copies)
|
||||
models = m.Models()
|
||||
models_l = []
|
||||
while not models.empty():
|
||||
models_l.append(models.pop())
|
||||
# Look for all the 3D models for this footprint
|
||||
for m3d in models_l:
|
||||
full_name = KiConf.expand_env(m3d.m_Filename)
|
||||
if not os.path.isfile(full_name):
|
||||
# Missing 3D model
|
||||
if full_name not in downloaded:
|
||||
logger.warning(W_MISS3D+'Missing 3D model for {}: `{}`'.format(ref, full_name))
|
||||
if self.download and m3d.m_Filename.startswith('${KISYS3DMOD}/'):
|
||||
# This is a model from KiCad, try to download it
|
||||
fname = m3d.m_Filename[14:]
|
||||
replace = None
|
||||
if full_name in downloaded:
|
||||
# Already downloaded
|
||||
replace = os.path.join(self._tmp_dir, fname)
|
||||
else:
|
||||
# Download the model
|
||||
url = self.kicad_3d_url+fname
|
||||
replace = self.download_model(url, fname)
|
||||
if replace:
|
||||
# Successfully downloaded
|
||||
downloaded.add(full_name)
|
||||
self.undo_3d_models[replace] = m3d.m_Filename
|
||||
# If this is a .wrl also download the .step
|
||||
if url.endswith('.wrl'):
|
||||
url = url[:-4]+'.step'
|
||||
fname = fname[:-4]+'.step'
|
||||
self.download_model(url, fname)
|
||||
if replace:
|
||||
m3d.m_Filename = replace
|
||||
models_replaced = True
|
||||
# Push the models back
|
||||
for model in models_l:
|
||||
models.push_front(model)
|
||||
return models_replaced
|
||||
|
||||
def list_models(self):
|
||||
""" Get the list of 3D models """
|
||||
# Load KiCad configuration so we can expand the 3D models path
|
||||
KiConf.init(GS.pcb_file)
|
||||
models = set()
|
||||
# Look for all the footprints
|
||||
for m in GS.board.GetModules():
|
||||
# Look for all the 3D models for this footprint
|
||||
for m3d in m.Models():
|
||||
full_name = KiConf.expand_env(m3d.m_Filename)
|
||||
if os.path.isfile(full_name):
|
||||
models.add(full_name)
|
||||
return list(models)
|
||||
|
||||
def save_board(self, dir):
|
||||
""" Save the PCB to a temporal file """
|
||||
with NamedTemporaryFile(mode='w', suffix='.kicad_pcb', delete=False, dir=dir) as f:
|
||||
fname = f.name
|
||||
logger.debug('Storing modified PCB to `{}`'.format(fname))
|
||||
GS.board.Save(fname)
|
||||
return fname
|
||||
|
||||
def filter_components(self, dir):
|
||||
self.undo_3d_models_rep = {}
|
||||
if not self._comps:
|
||||
# No variant/filter to apply
|
||||
if self.download_models():
|
||||
# Some missing components found and we downloaded them
|
||||
# Save the fixed board
|
||||
ret = self.save_board(dir)
|
||||
# Undo the changes
|
||||
self.undo_3d_models_rename()
|
||||
return ret
|
||||
return GS.pcb_file
|
||||
comps_hash = self.get_refs_hash()
|
||||
# Remove the 3D models for not fitted components
|
||||
rem_models = []
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
c = comps_hash.get(ref, None)
|
||||
if c:
|
||||
# The filter/variant knows about this component
|
||||
models = m.Models()
|
||||
if c.included and not c.fitted:
|
||||
# Not fitted, remove the 3D model
|
||||
rem_m_models = []
|
||||
while not models.empty():
|
||||
rem_m_models.append(models.pop())
|
||||
rem_models.append(rem_m_models)
|
||||
else:
|
||||
# Fitted
|
||||
new_model = c.get_field_value(GS.global_3D_model_field)
|
||||
if new_model:
|
||||
# We will change the 3D model
|
||||
self.replace_models(models, new_model, c)
|
||||
self.download_models()
|
||||
fname = self.save_board(dir)
|
||||
self.undo_3d_models_rename()
|
||||
# Undo the removing
|
||||
for m in GS.board.GetModules():
|
||||
ref = m.GetReference()
|
||||
c = comps_hash.get(ref, None)
|
||||
if c and c.included and not c.fitted:
|
||||
models = m.Models()
|
||||
restore = rem_models.pop(0)
|
||||
for model in restore:
|
||||
models.push_front(model)
|
||||
return fname
|
||||
|
||||
def get_targets(self, out_dir):
|
||||
return [self._parent.expand_filename(out_dir, self.output)]
|
||||
|
||||
def run(self, output):
|
||||
super().run(output)
|
||||
# Make units explicit
|
||||
|
|
@ -286,7 +88,7 @@ class STEPOptions(VariantOptions):
|
|||
|
||||
|
||||
@output_class
|
||||
class STEP(BaseOutput): # noqa: F821
|
||||
class STEP(Base3D): # noqa: F821
|
||||
""" STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure)
|
||||
Exports the PCB as a 3D model.
|
||||
This is the most common 3D format for exchange purposes.
|
||||
|
|
@ -296,8 +98,3 @@ class STEP(BaseOutput): # noqa: F821
|
|||
with document:
|
||||
self.options = STEPOptions
|
||||
""" [dict] Options for the `step` output """
|
||||
|
||||
def get_dependencies(self):
|
||||
files = super().get_dependencies()
|
||||
files.extend(self.options.list_models())
|
||||
return files
|
||||
|
|
|
|||
Loading…
Reference in New Issue