KiBot/kibot/out_blender_export.py

748 lines
32 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2023 Salvador E. Tropea
# Copyright (c) 2023 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
Dependencies:
- from: Blender
role: mandatory
version: 3.4.0
- from: ImageMagick
role: Automatically crop images
"""
import json
import os
import re
from tempfile import NamedTemporaryFile, TemporaryDirectory
from .error import KiPlotConfigurationError
from .kiplot import get_output_targets, run_output, run_command, register_xmp_import, config_output, configure_and_run
from .gs import GS
from .misc import MISSING_TOOL, BLENDER_ERROR
from .optionable import Optionable, BaseOptions
from .out_base_3d import Base3D, Base3DOptionsWithHL
from .registrable import RegOutput
from .macros import macros, document, output_class # noqa: F401
from . import log
logger = log.get_logger()
bb = None
RE_FILE_ID = re.compile(r"\%\d*d")
def get_board_size():
global bb
if bb is None:
bb = GS.board.ComputeBoundingBox(True)
width = GS.to_mm(bb.GetWidth())/1000.0
height = GS.to_mm(bb.GetHeight())/1000.0
size = max(width, height)
return width, height, size
class PCB2BlenderOptions(Optionable):
""" How the PCB3D is imported """
def __init__(self):
super().__init__()
with document:
self.components = True
""" Import the components """
self.cut_boards = True
""" Separate the sub-PCBs in separated 3D models """
self.texture_dpi = 1016.0
""" [508-2032] Texture density in dots per inch """
self.center = True
""" Center the PCB at the coordinates origin """
self.enhance_materials = True
""" Create good looking materials """
self.merge_materials = True
""" Reuse materials """
self.solder_joints = "SMART"
""" [NONE,SMART,ALL] The plug-in can add nice looking solder joints.
This option controls if we add it for none, all or only for THT/SMD pads with solder paste """
self.stack_boards = True
""" Move the sub-PCBs to their relative position """
self._unknown_is_error = True
class BlenderOutputOptions(Optionable):
""" What is generated """
def __init__(self):
super().__init__()
with document:
self.type = 'render'
""" *[fbx,obj,x3d,gltf,stl,ply,blender,render] The format for the output.
The `render` type will generate a PNG image of the render result.
`fbx` is Kaydara's Filmbox, 'obj' is the Wavefront, 'x3d' is the new ISO/IEC standard
that replaced VRML, `gltf` is the standardized GL format, `stl` is the 3D printing
format, 'ply' is Polygon File Format (Stanford).
Note that some formats includes the light and camera and others are just the 3D model
(i.e. STL and PLY) """
self.output = GS.def_global_output
""" Name for the generated file (%i='3D_blender_$VIEW' %x=VARIABLE).
The extension is selected from the type """
self.dir = ''
""" Subdirectory for this output """
self._unknown_is_error = True
class BlenderObjOptions(Optionable):
""" A light/camera in the scene. """
def __init__(self):
super().__init__()
with document:
self.name = ""
""" Name for the """
self.pos_x = 0
""" [number|string] X position [m]. You can use `width`, `height` and `size` for PCB dimensions """
self.pos_y = 0
""" [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions """
self.pos_z = 0
""" [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions """
self._unknown_is_error = True
def solve(self, member):
val = getattr(self, member)
if not isinstance(val, str):
return float(val)
try:
res = eval(val, {}, {'width': self._width, 'height': self._width, 'size': self._size})
except Exception as e:
raise KiPlotConfigurationError('wrong `{}`: `{}`\nPython says: `{}`'.format(member, val, str(e)))
return res
def config(self, parent):
super().config(parent)
self._width, self._height, self._size = get_board_size()
self.pos_x = self.solve('pos_x')
self.pos_y = self.solve('pos_y')
self.pos_z = self.solve('pos_z')
class BlenderLightOptions(BlenderObjOptions):
""" A light in the scene. """
def __init__(self):
super().__init__()
with document:
self.type = "POINT"
""" [POINT,SUN,SPOT,HEMI,AREA] Type of light. SUN lights will illuminate more evenly """
self.energy = 0
""" How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful """
self.add_to_doc('name', ' light', with_nl=False)
def adjust(self):
self._width, self._height, self._size = get_board_size()
if not self.get_user_defined('pos_x') and not self.get_user_defined('pos_y') and not self.get_user_defined('pos_z'):
self.pos_x = -self._size*3.33
self.pos_y = self._size*3.33
self.pos_z = self._size*5.0
if self.energy == 0:
if self.type == "POINT":
# 10 W is the default, works for 5 cm board, we make it grow cudratically
self.energy = 10.0*((self._size/0.05)**2)
elif self.type == "SUN":
# This is power by area
self.energy = 2
def config(self, parent):
super().config(parent)
self.adjust()
class BlenderCameraOptions(BlenderObjOptions):
""" A camera in the scene. """
CAM_TYPE = {'perspective': 'PERSP', 'orthographic': 'ORTHO', 'panoramic': 'PANO'}
def __init__(self):
super().__init__()
with document:
self.type = "perspective"
""" [perspective,orthographic,panoramic] Type of camera """
self.clip_start = -1
""" Minimum distance for objects to the camera. Any object closer than this distance won't be visible.
Only positive values have effect. A negative value has a special meaning.
For a camera with defined position, a negative value means to use Blender defaults (i.e. 0.1 m).
For cameras without position KiBot will ask Blender to compute its position and the use a clip
distance that is 1/10th of the Z distance """
self.add_to_doc('name', ' camera', with_nl=False)
def config(self, parent):
super().config(parent)
self._type = self.CAM_TYPE[self.type]
class BlenderRenderOptions(Optionable):
""" Render parameters """
def __init__(self):
super().__init__()
with document:
self.samples = 10
""" *How many samples we create. Each sample is a raytracing render.
Use 1 for a raw preview, 10 for a draft and 100 or more for the final render """
self.resolution_x = 1280
""" Width of the image """
self.width = None
""" {resolution_x} """
self.resolution_y = 720
""" Height of the image """
self.height = None
""" {resolution_y} """
self.transparent_background = False
""" *Make the background transparent """
self.background1 = "#66667F"
""" First color for the background gradient """
self.background2 = "#CCCCE5"
""" Second color for the background gradient """
self.auto_crop = False
""" When enabled the image will be post-processed to remove the empty space around the image.
In this mode the `background2` is changed to be the same as `background1` """
self.no_denoiser = False
""" Used to disable the render denoiser on old hardware, or when the functionality isn't compiled.
Note that the impact in quality is huge, you should increase the amount of samples 10 times """
self._unknown_is_error = True
class BlenderPointOfViewOptions(Optionable):
""" Point of View parameters """
_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):
super().__init__()
with document:
self.rotate_x = 0
""" Angle to rotate the board in the X axis, positive is clockwise [degrees] """
self.rotate_y = 0
""" Angle to rotate the board in the Y axis, positive is clockwise [degrees] """
self.rotate_z = 0
""" Angle to rotate the board in the Z axis, positive is clockwise [degrees] """
self.view = 'top'
""" *[top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
Compatible with `render_3d` """
self.file_id = ''
""" String to differentiate the name of this point of view.
When empty we use the `default_file_id` or the `view` """
self.steps = 1
""" [1-1000] Generate this amount of steps using the rotation angles as increments.
Use a value of 1 (default) to interpret the angles as absolute.
Used for animations. You should define the `default_file_id` to something like
'_%03d' to get the animation frames """
self._unknown_is_error = True
def config(self, parent):
super().config(parent)
# View point
view = self._views.get(self.view, None)
if view is not None:
self.view = view
def get_view(self):
return self._rviews.get(self.view, 'no_view')
def increment(self, inc):
self.rotate_x += inc.rotate_x
self.rotate_y += inc.rotate_y
self.rotate_z += inc.rotate_z
class PCB3DExportOptions(Base3DOptionsWithHL):
""" Options to generate the PCB3D file """
def __init__(self):
super().__init__()
with document:
self.output = GS.def_global_output
""" Name for the generated PCB3D file (%i='blender_export' %x='pcb3d') """
self.version = '2.7'
""" [2.1,2.1_haschtl,2.7] Variant of the format used """
self.solder_paste_for_populated = True
""" Add solder paste only for the populated components.
Populated components are the ones listed in `show_components` """
self._expand_id = 'blender_export'
self._expand_ext = 'pcb3d'
self._unknown_is_error = True
def get_output_name(self, out_dir):
p = self._parent
cur_id = p._expand_id
cur_ext = p._expand_ext
p._expand_id = self._expand_id
p._expand_ext = self._expand_ext
out_name = p._parent.expand_filename(out_dir, self.output)
p._expand_id = cur_id
p._expand_ext = cur_ext
return out_name
def setup_renderer(self, components, active_components, bottom, name):
super().setup_renderer(components, active_components)
self._pov.view = 'Z' if bottom else 'z'
# Expand the name using .PNG
cur_ext = self._expand_ext
self._expand_ext = 'png'
o_name = self.expand_filename_both(name, is_sch=False)
self._expand_ext = cur_ext
self._out.output = o_name
return o_name
def save_renderer_options(self):
""" Save the current renderer settings """
p = self._parent
# We are an option inside another option
self._parent = self._parent._parent
super().save_renderer_options()
self._parent = p
self.old_show_all_components = self._show_all_components
self.old_view = self._pov.view
self.old_output = self._out.output
def restore_renderer_options(self):
""" Restore the renderer settings """
p = self._parent
self._parent = self._parent._parent
super().restore_renderer_options()
self._parent = p
self._show_all_components = self.old_show_all_components
self._pov.view = self.old_view
self._out.output = self.old_output
class Blender_ExportOptions(BaseOptions):
def __init__(self):
with document:
self.pcb3d = PCB3DExportOptions
""" *[string|dict] Options to export the PCB to Blender.
You can also specify the name of the output that generates the PCB3D file.
See the `PCB2Blender_2_1`, `PCB2Blender_2_7` and `PCB2Blender_2_1_haschtl` templates """
self.pcb_import = PCB2BlenderOptions
""" Options to configure how Blender imports the PCB.
The default values are good for most cases """
self.outputs = BlenderOutputOptions
""" [dict|list(dict)] Outputs to generate in the same run """
self.light = BlenderLightOptions
""" [dict|list(dict)] Options for the light/s """
self.add_default_light = True
""" Add a default light when none specified.
The default light is located at (-size*3.33, size*3.33, size*5) where size is max(width, height) of the PCB """
self.camera = BlenderCameraOptions
""" [dict] Options for the camera.
If none specified KiBot will create a suitable camera.
If no position is specified for the camera KiBot will look for a suitable position """
self.fixed_auto_camera = False
""" When using the automatically generated camera and multiple points of view this option computes the camera
position just once. Suitable for videos """
self.auto_camera_z_axis_factor = 1.1
""" Value to multiply the Z axis coordinate after computing the automatically generated camera.
Used to avoid collision of the camera and the object """
self.default_file_id = ''
""" Default value for the `file_id` in the `point_of_view` options.
Use something like '_%03d' for animations """
self.render_options = BlenderRenderOptions
""" *[dict] Controls how the render is done for the `render` output type """
self.point_of_view = BlenderPointOfViewOptions
""" *[dict|list(dict)] How the object is viewed by the camera """
super().__init__()
self._expand_id = '3D_blender'
self._unknown_is_error = True
def config(self, parent):
super().config(parent)
# Check we at least have a name for the source output
if isinstance(self.pcb3d, str) and not self.pcb3d:
raise KiPlotConfigurationError('You must specify the name of the output that'
' generates the PCB3D file or its options')
if isinstance(self.pcb3d, type):
self.pcb3d = PCB3DExportOptions()
self.pcb3d.config(self)
# Do we have outputs?
if isinstance(self.outputs, type):
self.outputs = []
elif isinstance(self.outputs, BlenderOutputOptions):
# One, make a list
self.outputs = [self.outputs]
# Ensure we have import options
if isinstance(self.pcb_import, type):
self.pcb_import = PCB2BlenderOptions()
# Ensure we have a light
if isinstance(self.light, type):
# None
if self.add_default_light:
# Create one
light = BlenderLightOptions()
light.name = 'kibot_light'
light.adjust()
self.light = [light]
else:
# The dark ...
self.light = []
elif isinstance(self.light, BlenderLightOptions):
# Ensure a list
self.light = [self.light]
# Check light names
light_names = set()
for li in self.light:
name = li.name if li.name else 'kibot_light'
if name in light_names:
id = 2
while name+'_'+str(id) in light_names:
id += 1
name = name+'_'+str(id)
li.name = name
# If no camera let the script create one
if isinstance(self.camera, type):
self.camera = None
elif not self.camera.name:
self.camera.name = 'kibot_camera'
# Ensure we have some render options
if isinstance(self.render_options, type):
self.render_options = BlenderRenderOptions()
# Point of View, make sure we have a list and at least 1 element
if isinstance(self.point_of_view, type):
pov = BlenderPointOfViewOptions()
# Manually translate top -> z
pov.view = 'z'
self.point_of_view = [pov]
elif isinstance(self.point_of_view, BlenderPointOfViewOptions):
self.point_of_view = [self.point_of_view]
def get_output_filename(self, o, output_dir, pov, order):
if o.type == 'render':
self._expand_ext = 'png'
elif o.type == 'blender':
self._expand_ext = 'blend'
else:
self._expand_ext = o.type
cur_id = self._expand_id
file_id = pov.file_id
if not file_id:
file_id = self.default_file_id or ('_'+pov.get_view())
m = RE_FILE_ID.search(file_id)
if m:
res = m.group(0)
val = res % order
file_id = file_id.replace(res, val)
self._expand_id += file_id
name = self._parent.expand_filename(os.path.join(output_dir, o.dir), o.output)
self._expand_id = cur_id
return name
def get_targets(self, out_dir):
files = []
if isinstance(self.pcb3d, PCB3DExportOptions):
files.append(self.pcb3d.get_output_name(out_dir))
order = 1
for pov in self.point_of_view:
for _ in range(pov.steps):
for o in self.outputs:
files.append(self.get_output_filename(o, out_dir, pov, order))
return files
def create_vrml(self, dest_dir):
tree = {'name': '_temporal_vrml_for_pcb3d',
'type': 'vrml',
'comment': 'Internally created for the PCB3D',
'dir': dest_dir,
'options': {'output': 'pcb.wrl',
'dir_models': 'components',
'use_pcb_center_as_ref': False,
'model_units': 'meters'}}
out = RegOutput.get_class_for('vrml')()
out.set_tree(tree)
config_output(out)
out.options.copy_options(self.pcb3d)
logger.debug(' - Creating VRML ...')
out.options.run(os.path.join(dest_dir, 'pcb.wrl'))
def create_layers(self, dest_dir):
out_dir = os.path.join(dest_dir, 'layers')
tree = {'name': '_temporal_svgs_layers',
'type': 'svg',
'comment': 'Internally created for the PCB3D',
'dir': out_dir,
'options': {'output': '%i.%x',
'margin': 1,
'limit_viewbox': True,
'svg_precision': 6,
'drill_marks': 'none'},
'layers': ['F.Cu', 'B.Cu', 'F.Paste', 'B.Paste', 'F.Mask', 'B.Mask',
{'layer': 'F.SilkS', 'suffix': 'F_SilkS'},
{'layer': 'B.SilkS', 'suffix': 'B_SilkS'}]}
configure_and_run(tree, out_dir, ' - Creating SVG for layers ...')
def create_pads(self, dest_dir):
options = {'stackup_create': False}
if self.pcb3d.version == '2.1_haschtl':
options['stackup_create'] = True
elif self.pcb3d.version == '2.7':
options['stackup_create'] = True
options['stackup_file'] = 'stackup'
options['stackup_dir'] = 'layers'
options['stackup_format'] = 'BIN'
tree = {'name': '_temporal_pcb3d_tools',
'type': 'pcb2blender_tools',
'comment': 'Internally created for the PCB3D',
'dir': dest_dir,
'options': options}
if self.pcb3d.solder_paste_for_populated:
sc = 'all'
if not self.pcb3d._show_all_components:
sc = 'none' if not self.pcb3d.show_components else self.pcb3d._show_components_raw
tree['options']['show_components'] = sc
configure_and_run(tree, dest_dir, ' - Creating Pads and boundary ...')
def create_pcb3d(self, data_dir):
out_dir = self._parent.output_dir
# Compute the name for the PCB3D
out_name = self.pcb3d.get_output_name(out_dir)
tree = {'name': '_temporal_compress_pcb3d',
'type': 'compress',
'comment': 'Internally created for the PCB3D',
'dir': out_dir,
'options': {'output': out_name,
'format': 'ZIP',
'files': [{'source': os.path.join(data_dir, 'boards'),
'dest': '/'},
{'source': os.path.join(data_dir, 'boards/*'),
'dest': 'boards'},
{'source': os.path.join(data_dir, 'components'),
'dest': '/'},
{'source': os.path.join(data_dir, 'components/*'),
'dest': 'components'},
{'source': os.path.join(data_dir, 'layers'),
'dest': '/'},
{'source': os.path.join(data_dir, 'layers/*'),
'dest': 'layers'},
{'source': os.path.join(data_dir, 'pads'),
'dest': '/'},
{'source': os.path.join(data_dir, 'pads/*'),
'dest': 'pads'},
{'source': os.path.join(data_dir, 'pcb.wrl'),
'dest': '/'},
]}}
configure_and_run(tree, out_dir, ' - Creating the PCB3D ...')
return out_name
def solve_pcb3d(self):
if isinstance(self.pcb3d, str):
# An output creates it
pcb3d_targets, _, pcb3d_out = get_output_targets(self.pcb3d, self._parent)
pcb3d_file = pcb3d_targets[0]
logger.debug('- From file '+pcb3d_file)
if not pcb3d_out._done:
logger.debug('- Running '+self.pcb3d)
run_output(pcb3d_out)
self._pcb3d = PCB3DExportOptions()
self._pcb3d.output = pcb3d_file
# Needed by ensure tool
self._pcb3d._parent = self._parent
else:
# We create it
with TemporaryDirectory() as tmp_dir:
# VRML
self.create_vrml(tmp_dir)
# SVG layers
self.create_layers(tmp_dir)
# Pads and bounds
self.create_pads(tmp_dir)
# Compress the files
pcb3d_file = self.create_pcb3d(tmp_dir)
self._pcb3d = self.pcb3d
# Needed by ensure tool
self.type = self._parent.type
if not os.path.isfile(pcb3d_file):
raise KiPlotConfigurationError('Missing '+pcb3d_file)
return pcb3d_file
def analyze_errors(self, msg):
if 'Traceback ' in msg:
GS.exit_with_error('Error from Blender run:\n'+msg[msg.index('Traceback '):], BLENDER_ERROR)
def run(self, output):
if GS.ki5:
GS.exit_with_error("`blender_export` needs KiCad 6+", MISSING_TOOL)
pcb3d_file = self.solve_pcb3d()
# If no outputs specified just finish
# Can be used to export the PCB to Blender
if not self.outputs:
return
# Make sure Blender is available
command = self._pcb3d.ensure_tool('Blender')
if self.render_options.auto_crop:
# Avoid a gradient
self.render_options.background2 = self.render_options.background1
convert_command = self.ensure_tool('ImageMagick')
# Create a JSON with the scene information
with NamedTemporaryFile(mode='w', suffix='.json') as f:
scene = {}
if self.light:
lights = [{'name': light.name,
'position': (light.pos_x, light.pos_y, light.pos_z),
'type': light.type,
'energy': light.energy} for light in self.light]
scene['lights'] = lights
if self.camera:
ca = self.camera
scene['camera'] = {'name': ca.name,
'type': ca._type}
if (hasattr(ca, '_pos_x_user_defined') or hasattr(ca, '_pos_y_user_defined') or
hasattr(ca, '_pos_z_user_defined')):
scene['camera']['position'] = (ca.pos_x, ca.pos_y, ca.pos_z)
if ca.clip_start >= 0:
scene['camera']['clip_start'] = ca.clip_start
scene['fixed_auto_camera'] = self.fixed_auto_camera
scene['auto_camera_z_axis_factor'] = self.auto_camera_z_axis_factor
ro = self.render_options
scene['render'] = {'samples': ro.samples,
'resolution_x': ro.resolution_x,
'resolution_y': ro.resolution_y,
'transparent_background': ro.transparent_background,
'background1': ro.background1,
'background2': ro.background2}
povs = []
last_pov = BlenderPointOfViewOptions()
for pov in self.point_of_view:
if pov.steps > 1:
for _ in range(pov.steps):
last_pov.increment(pov)
povs.append({'rotate_x': -last_pov.rotate_x,
'rotate_y': -last_pov.rotate_y,
'rotate_z': -last_pov.rotate_z,
'view': pov.view})
else:
povs.append({'rotate_x': -pov.rotate_x,
'rotate_y': -pov.rotate_y,
'rotate_z': -pov.rotate_z,
'view': pov.view})
last_pov = pov
scene['point_of_view'] = povs
text = json.dumps(scene, sort_keys=True, indent=2)
logger.debug('Scene:\n'+text)
f.write(text)
f.flush()
# Create the command line
script = os.path.join(os.path.dirname(__file__), 'blender_scripts', 'blender_export.py')
cmd = [command, '-b', '--factory-startup', '-P', script, '--']
pi = self.pcb_import
if not pi.components:
cmd.append('--no_components')
if not pi.cut_boards:
cmd.append('--dont_cut_boards')
if pi.texture_dpi != 1016.0:
cmd.extend(['--texture_dpi', str(pi.texture_dpi)])
if not pi.center:
cmd.append('--dont_center')
if not pi.enhance_materials:
cmd.append('--dont_enhance_materials')
if not pi.merge_materials:
cmd.append('--dont_merge_materials')
if pi.solder_joints != "SMART":
cmd.extend(['--solder_joints', pi.solder_joints])
if not pi.stack_boards:
cmd.append('--dont_stack_boards')
if self.render_options.no_denoiser:
cmd.append('--no_denoiser')
cmd.append('--format')
for pov in self.point_of_view:
for _ in range(pov.steps):
for o in self.outputs:
cmd.append(o.type)
cmd.append('--output')
names = set()
order = 1
for pov in self.point_of_view:
for _ in range(pov.steps):
for o in self.outputs:
name = self.get_output_filename(o, self._parent.output_dir, pov, order)
if name in names:
raise KiPlotConfigurationError('Repeated name (use `file_id`): '+name)
cmd.append(name)
names.add(name)
os.makedirs(os.path.dirname(name), exist_ok=True)
order += 1
cmd.extend(['--scene', f.name])
cmd.append(pcb3d_file)
# Execute the command
self.analyze_errors(run_command(cmd))
if self.render_options.auto_crop:
for pov in self.point_of_view:
for o in self.outputs:
if o.type != 'render':
continue
name = self.get_output_filename(o, self._parent.output_dir, pov)
run_command([convert_command, name, '-trim', '+repage', '-trim', '+repage', name])
@output_class
class Blender_Export(Base3D):
""" Blender Export
Exports the PCB in various 3D file formats.
Also renders the PCB with high-quality.
Needs KiCad 6 or newer.
This output is complex to setup and needs very big dependencies.
Please be patient when using it.
You need Blender with the pcb2blender plug-in installed.
Visit: [pcb2blender](https://github.com/30350n/pcb2blender).
You can just generate the exported PCB if no output is specified.
You can also export the PCB and render it at the same time """
def __init__(self):
super().__init__()
with document:
self.options = Blender_ExportOptions
""" *[dict] Options for the `blender_export` output """
self._category = 'PCB/3D'
def get_dependencies(self):
files = BaseOutput.get_dependencies(self) # noqa: F821
if isinstance(self.options.pcb3d, str):
files.append(self.options.pcb3d)
else:
files.extend(self.options.pcb3d.list_models())
return files
def get_renderer_options(self):
""" Where are the options for this output when used as a 'renderer' """
ops = self.options
out = next(filter(lambda x: x.type == 'render', ops.outputs), None)
res = None
if out is not None:
if isinstance(ops.pcb3d, str):
# We can't configure it
out = None
else:
res = ops.pcb3d
res._pov = ops.point_of_view[0]
res._out = out
return res if out is not None else None
def get_extension(self):
# Used when we are a renderer
return 'png'
@staticmethod
def get_conf_examples(name, layers):
if not GS.check_tool(name, 'Blender') or GS.ki5:
return None
has_top = False
has_bottom = False
for la in layers:
if la.is_top() or la.layer.startswith('F.'):
has_top = True
elif la.is_bottom() or la.layer.startswith('B.'):
has_bottom = True
if not has_top and not has_bottom:
return None
register_xmp_import('PCB2Blender_2_1')
povs = []
if has_top:
povs.append({'view': 'top'})
povs.append({'rotate_x': 30, 'rotate_z': -20, 'file_id': '_30deg'})
if has_bottom:
povs.append({'view': 'bottom'})
gb = {}
gb['name'] = 'basic_{}'.format(name)
gb['comment'] = '3D view from top/30 deg/bottom (Blender)'
gb['type'] = name
gb['dir'] = '3D'
gb['options'] = {'pcb3d': '_PCB2Blender_2_1',
'outputs': [{'type': 'render'}, {'type': 'blender'}],
'point_of_view': povs}
return [gb]