[Blender Export][Added] Options useful to generate simple animations

- The resulting PNGs can be converted to MP4 using ffmpeg
This commit is contained in:
Salvador E. Tropea 2023-08-16 13:36:19 -03:00
parent 4202f01c01
commit 948a40fb91
6 changed files with 246 additions and 113 deletions

View File

@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(#470) (#470)
- Blender export: - Blender export:
- Support for pcb2blender v2.6 (Blender 3.5.1) - Support for pcb2blender v2.6 (Blender 3.5.1)
- `auto_camera_z_axis_factor`: used to control the default camera distance
- Options to create simple animations:
- PoV `steps`: to create rotation angle increments
- `default_file_id`: can be used to create numbered PNGs
- `fixed_auto_camera`: to avoid adjusting the automatic camera on each frame
- Populate: - Populate:
- Basic support for regular list items (#480) - Basic support for regular list items (#480)
@ -26,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Problems when trying to aggregate the datasheet field (#472) - Problems when trying to aggregate the datasheet field (#472)
- kibot-check: - kibot-check:
- Show 7.x as supported (#469) - Show 7.x as supported (#469)
- Blender export:
- Rotations are now applied to the current view, not just the top view
## [1.6.3] - 2023-06-26 ## [1.6.3] - 2023-06-26

View File

@ -1736,11 +1736,15 @@ Notes:
* Valid keys: * Valid keys:
- **`view`**: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view. - **`view`**: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
Compatible with `render_3d`. Compatible with `render_3d`.
- `file_id`: [string=''] String to diferentiate the name of this view. - `file_id`: [string=''] String to diferentiate the name of this point of view.
When empty we use the `view`. When empty we use the `default_file_id` or the `view`.
- `rotate_x`: [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]. - `rotate_x`: [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees].
- `rotate_y`: [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees]. - `rotate_y`: [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees].
- `rotate_z`: [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]. - `rotate_z`: [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees].
- `steps`: [number=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.
- **`render_options`**: [dict] Controls how the render is done for the `render` output type. - **`render_options`**: [dict] Controls how the render is done for the `render` output type.
* Valid keys: * Valid keys:
- **`samples`**: [number=10] How many samples we create. Each sample is a raytracing render. - **`samples`**: [number=10] How many samples we create. Each sample is a raytracing render.
@ -1756,6 +1760,8 @@ Notes:
- *width*: Alias for resolution_x. - *width*: Alias for resolution_x.
- `add_default_light`: [boolean=true] Add a default light when none specified. - `add_default_light`: [boolean=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. The default light is located at (-size*3.33, size*3.33, size*5) where size is max(width, height) of the PCB.
- `auto_camera_z_axis_factor`: [number=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.
- `camera`: [dict] Options for the camera. - `camera`: [dict] Options for the camera.
If none specified KiBot will create a suitable 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. If no position is specified for the camera KiBot will look for a suitable position.
@ -1765,6 +1771,10 @@ Notes:
- `pos_y`: [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions. - `pos_y`: [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions.
- `pos_z`: [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions. - `pos_z`: [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions.
- `type`: [string='perspective'] [perspective,orthographic,panoramic] Type of camera. - `type`: [string='perspective'] [perspective,orthographic,panoramic] Type of camera.
- `default_file_id`: [string=''] Default value for the `file_id` in the `point_of_view` options.
Use something like '_%03d' for animations.
- `fixed_auto_camera`: [boolean=false] When using the automatically generated camera and multiple points of view this option computes the camera
position just once. Suitable for videos.
- `light`: [dict|list(dict)] Options for the light/s. - `light`: [dict|list(dict)] Options for the light/s.
* Valid keys: * Valid keys:
- `energy`: [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful. - `energy`: [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful.
@ -1959,8 +1969,8 @@ Notes:
- `logo_scale`: [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software. - `logo_scale`: [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software.
- `max_col_width`: [number=60] [20,999] Maximum column width (characters). - `max_col_width`: [number=60] [20,999] Maximum column width (characters).
- `mouser_link`: [string|list(string)=''] Column/s containing Mouser part numbers, will be linked to web page. - `mouser_link`: [string|list(string)=''] Column/s containing Mouser part numbers, will be linked to web page.
- `specs_columns`: [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references, - `specs_columns`: [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the
'Row' for the order and 'Sep' to separate groups at the same level. By default all are included. references, 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint', Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
'_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'. '_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'.
* Valid keys: * Valid keys:
@ -3370,8 +3380,8 @@ Notes:
Plugin: Uses an external python function, only `code` and `arg` are relevant. Plugin: Uses an external python function, only `code` and `arg` are relevant.
- `arg`: [string=''] Argument to pass to the plugin. Used for *plugin*. - `arg`: [string=''] Argument to pass to the plugin. Used for *plugin*.
- `code`: [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin*. - `code`: [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin*.
- `cutout`: [number|string] When your design features open pockets on the side, this parameter specifies extra cutout depth in order to - `cutout`: [number|string] When your design features open pockets on the side, this parameter specifies extra cutout
ensure that a sharp corner of the pocket can be milled. Used for *full*. depth in order to ensure that a sharp corner of the pocket can be milled. Used for *full*.
- `hcount`: [number=1] Number of tabs in the horizontal direction. Used for *fixed*. - `hcount`: [number=1] Number of tabs in the horizontal direction. Used for *fixed*.
- `hwidth`: [number|string] The width of tabs in the horizontal direction. Used for *fixed* and *spacing*. - `hwidth`: [number|string] The width of tabs in the horizontal direction. Used for *fixed* and *spacing*.
- *min_distance*: Alias for mindistance. - *min_distance*: Alias for mindistance.

View File

@ -110,6 +110,9 @@ outputs:
# [boolean=true] Add a default light when none specified. # [boolean=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 # The default light is located at (-size*3.33, size*3.33, size*5) where size is max(width, height) of the PCB
add_default_light: true add_default_light: true
# [number=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
auto_camera_z_axis_factor: 1.1
# [dict] Options for the camera. # [dict] Options for the camera.
# If none specified KiBot will create a suitable 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 # If no position is specified for the camera KiBot will look for a suitable position
@ -124,6 +127,12 @@ outputs:
pos_z: 0 pos_z: 0
# [string='perspective'] [perspective,orthographic,panoramic] Type of camera # [string='perspective'] [perspective,orthographic,panoramic] Type of camera
type: 'perspective' type: 'perspective'
# [string=''] Default value for the `file_id` in the `point_of_view` options.
# Use something like '_%03d' for animations
default_file_id: ''
# [boolean=false] When using the automatically generated camera and multiple points of view this option computes the camera
# position just once. Suitable for videos
fixed_auto_camera: false
# [dict|list(dict)] Options for the light/s # [dict|list(dict)] Options for the light/s
light: light:
# [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful # [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful
@ -214,8 +223,8 @@ outputs:
texture_dpi: 1016.0 texture_dpi: 1016.0
# [dict|list(dict)] How the object is viewed by the camera # [dict|list(dict)] How the object is viewed by the camera
point_of_view: point_of_view:
# [string=''] String to diferentiate the name of this view. # [string=''] String to diferentiate the name of this point of view.
# When empty we use the `view` # When empty we use the `default_file_id` or the `view`
- file_id: '' - file_id: ''
# [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees] # [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]
rotate_x: 0 rotate_x: 0
@ -223,6 +232,11 @@ outputs:
rotate_y: 0 rotate_y: 0
# [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees] # [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]
rotate_z: 0 rotate_z: 0
# [number=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
steps: 1
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view. # [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
# Compatible with `render_3d` # Compatible with `render_3d`
view: 'top' view: 'top'
@ -556,8 +570,8 @@ outputs:
# [boolean=false] Enable Specs worksheet creation. Contains specifications for the components. # [boolean=false] Enable Specs worksheet creation. Contains specifications for the components.
# Works with only some KiCost APIs # Works with only some KiCost APIs
specs: false specs: false
# [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references, # [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the
# 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included. # references, 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
# Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint', # Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
# '_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size' # '_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'
specs_columns: specs_columns:
@ -1882,8 +1896,8 @@ outputs:
arg: '' arg: ''
# [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin* # [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin*
code: '' code: ''
# [number|string] When your design features open pockets on the side, this parameter specifies extra cutout depth in order to # [number|string] When your design features open pockets on the side, this parameter specifies extra cutout
# ensure that a sharp corner of the pocket can be milled. Used for *full* # depth in order to ensure that a sharp corner of the pocket can be milled. Used for *full*
cutout: 1 cutout: 1
# [number=1] Number of tabs in the horizontal direction. Used for *fixed* # [number=1] Number of tabs in the horizontal direction. Used for *fixed*
hcount: 1 hcount: 1

View File

@ -20,9 +20,12 @@ import sys # to get command line args
import addon_utils import addon_utils
import bpy import bpy
X_AXIS = 0 # X_AXIS = 0
Y_AXIS = 1 # Y_AXIS = 1
Z_AXIS = 2 # Z_AXIS = 2
X_AXIS = 'X'
Y_AXIS = 'Y'
Z_AXIS = 'Z'
VALID_FORMATS = {'fbx': 'Filmbox, proprietary format developed by Kaydara (owned by Autodesk)', VALID_FORMATS = {'fbx': 'Filmbox, proprietary format developed by Kaydara (owned by Autodesk)',
'obj': 'geometry definition format developed by Wavefront Technologies. Currently open', 'obj': 'geometry definition format developed by Wavefront Technologies. Currently open',
'x3d': 'VRML successor. A royalty-free ISO/IEC standard for declaratively representing 3D graphics.', 'x3d': 'VRML successor. A royalty-free ISO/IEC standard for declaratively representing 3D graphics.',
@ -70,38 +73,26 @@ def render_export(render_path):
bpy.ops.render.render(write_still=True) bpy.ops.render.render(write_still=True)
def rot(axis, deg): def do_rotate(rots):
bpy.context.active_object.rotation_euler[axis] = math.radians(deg) bpy.ops.transform.rotate(value=math.radians(-rots[0]), orient_axis='X', center_override=(0, 0, 0))
bpy.ops.transform.rotate(value=math.radians(-rots[1]), orient_axis='Y', center_override=(0, 0, 0))
bpy.ops.transform.rotate(value=math.radians(-rots[2]), orient_axis='Z', center_override=(0, 0, 0))
def do_rotate(scene, name, id):
rotate = scene.get(name)
if rotate:
rot(id, rotate)
def do_point_of_view(scene, name): def do_point_of_view(scene, name):
view = scene.get(name) view = scene.get(name)
if view is None or view == 'z': if view is None or view == 'z':
return return (0, 0, 0)
if view == 'Z': if view == 'Z':
rot(Y_AXIS, 180) return (0, 180, 0)
return
if view == 'y': if view == 'y':
rot(X_AXIS, -90) return (-90, 0, 0)
return
if view == 'Y': if view == 'Y':
rot(X_AXIS, 90) return (90, 0, 180)
rot(Z_AXIS, 180)
return
if view == 'x': if view == 'x':
rot(Y_AXIS, 90) return (0, 90, 90)
rot(Z_AXIS, 90)
return
if view == 'X': if view == 'X':
rot(Y_AXIS, -90) return (0, -90, -90)
rot(Z_AXIS, -90)
return
def srgb_to_linearrgb(c): def srgb_to_linearrgb(c):
@ -191,18 +182,19 @@ def create_background_gradient(color1, color0):
jscene = None jscene = None
auto_camera = False auto_camera = False
cam_ob = None cam_ob = None
cur_rot = None
location = None
def apply_scene(file, n_view=0): def apply_start_scene(file):
# Loads scene # Loads scene
if not file: if not file:
return 1 return 1
global jscene global jscene
if jscene is None: with open(file, 'rt') as f:
with open(file, 'rt') as f: text = f.read()
text = f.read() print(text)
print(text) jscene = json.loads(text)
jscene = json.loads(text)
scene = bpy.context.scene scene = bpy.context.scene
# Select the board # Select the board
@ -210,72 +202,77 @@ def apply_scene(file, n_view=0):
bpy.ops.object.select_all(action='SELECT') bpy.ops.object.select_all(action='SELECT')
# Make sure we start rotations from 0 # Make sure we start rotations from 0
bpy.context.active_object.rotation_euler = (0, 0, 0) bpy.context.active_object.rotation_euler = (0, 0, 0)
global location
location = bpy.context.active_object.location.copy()
povs = jscene.get('point_of_view') povs = jscene.get('point_of_view')
if povs: if povs:
pov = povs[n_view] global cur_rot
pov = povs[0]
# Apply point of view # Apply point of view
do_point_of_view(pov, 'view') cur_rot = do_point_of_view(pov, 'view')
# Apply extra rotations # Apply extra rotations
do_rotate(pov, 'rotate_x', 0) cur_rot = (cur_rot[0] + pov.get('rotate_x', 0),
do_rotate(pov, 'rotate_y', 1) cur_rot[1] + pov.get('rotate_y', 0),
do_rotate(pov, 'rotate_z', 2) cur_rot[2] + pov.get('rotate_z', 0))
print(f'- Initial rotations: {cur_rot}')
do_rotate(cur_rot)
# Add a camera # Add a camera
global auto_camera global auto_camera
if not n_view: # First time: create the camera
# First time: create the camera camera = jscene.get('camera')
camera = jscene.get('camera') if not camera:
if not camera: auto_camera = True
name = 'kibot_camera'
pos = (0.0, 0.0, 10.0)
type = 'PERSP'
else:
name = camera.get('name', 'unknown')
pos = camera.get('position', None)
type = camera.get('type', 'PERSP')
if pos is None:
auto_camera = True auto_camera = True
name = 'kibot_camera' pos = (0, 0, 0)
pos = (0.0, 0.0, 10.0)
type = 'PERSP'
else: else:
name = camera.get('name', 'unknown') auto_camera = False
pos = camera.get('position', None) print(f"- Creating camera {name} at {pos}")
type = camera.get('type', 'PERSP') cam_data = bpy.data.cameras.new(name)
if pos is None: global cam_ob
auto_camera = True cam_ob = bpy.data.objects.new(name=name, object_data=cam_data)
pos = (0, 0, 0) scene.collection.objects.link(cam_ob) # instance the camera object in the scene
else: scene.camera = cam_ob # set the active camera
auto_camera = False cam_ob.location = pos
print(f"- Creating camera {name} at {pos}") cam_ob.data.type = type
cam_data = bpy.data.cameras.new(name)
global cam_ob
cam_ob = bpy.data.objects.new(name=name, object_data=cam_data)
scene.collection.objects.link(cam_ob) # instance the camera object in the scene
scene.camera = cam_ob # set the active camera
cam_ob.location = pos
cam_ob.data.type = type
if auto_camera: if auto_camera:
print('- Changing camera to focus the board') print('- Changing camera to focus the board')
bpy.ops.view3d.camera_to_view_selected() bpy.ops.view3d.camera_to_view_selected()
cam_ob.location = (cam_ob.location[0], cam_ob.location[1], cam_ob.location[2]*1.1) cam_ob.location = (cam_ob.location[0], cam_ob.location[1],
cam_ob.location[2]*jscene.get('auto_camera_z_axis_factor', 1.1))
# Add lights # Add lights
if not n_view: # First time: create the lights
# First time: create the lights lights = jscene.get('lights')
lights = jscene.get('lights') if lights:
if lights: for light in lights:
for light in lights: name = light.get('name', 'unknown')
name = light.get('name', 'unknown') pos = light.get('position', (0.0, 0.0, 0.0))
pos = light.get('position', (0.0, 0.0, 0.0)) typ = light.get('type', 'SUN')
typ = light.get('type', 'SUN') energy = light.get('energy', 0.0)
energy = light.get('energy', 0.0) print(f"- Creating light {name} at {pos}, type: {typ} energy: {energy}")
print(f"- Creating light {name} at {pos}, type: {typ} energy: {energy}") light_data = bpy.data.lights.new(name, typ)
light_data = bpy.data.lights.new(name, typ) print(f"- Default energy: {light_data.energy}")
print(f"- Default energy: {light_data.energy}") if energy:
if energy: light_data.energy = energy
light_data.energy = energy light_ob = bpy.data.objects.new(name=name, object_data=light_data)
light_ob = bpy.data.objects.new(name=name, object_data=light_data) scene.collection.objects.link(light_ob)
scene.collection.objects.link(light_ob) light_ob.location = pos
light_ob.location = pos
bpy.context.view_layer.update() bpy.context.view_layer.update()
# Setup render options # Setup render options
render = jscene.get('render') render = jscene.get('render')
if render and not n_view: if render:
scene.cycles.samples = render.get('samples', 10) scene.cycles.samples = render.get('samples', 10)
r = scene.render r = scene.render
r.engine = 'CYCLES' r.engine = 'CYCLES'
@ -291,6 +288,43 @@ def apply_scene(file, n_view=0):
return len(povs) return len(povs)
def apply_scene(n_view):
global jscene
if jscene is None:
return
# Make sure we start rotations from 0
povs = jscene.get('point_of_view')
if povs:
global cur_rot
pov = povs[n_view]
# Apply point of view
new_rot = do_point_of_view(pov, 'view')
# Apply extra rotations
new_rot = (new_rot[0] + pov.get('rotate_x', 0),
new_rot[1] + pov.get('rotate_y', 0),
new_rot[2] + pov.get('rotate_z', 0))
if new_rot != cur_rot:
print(f'- Rotations: {new_rot}')
# Reset the position
bpy.context.active_object.location = location.copy()
# Reset the rotation
bpy.context.active_object.rotation_euler = (0, 0, 0)
# Apply the new rotation
do_rotate(new_rot)
cur_rot = new_rot
# Move the camera
global auto_camera
if auto_camera and not jscene.get('fixed_auto_camera', False):
print('- Changing camera to focus the board')
bpy.ops.view3d.camera_to_view_selected()
cam_ob.location = (cam_ob.location[0], cam_ob.location[1],
cam_ob.location[2]*jscene.get('auto_camera_z_axis_factor', 1.1))
bpy.context.view_layer.update()
EXPORTERS = {'fbx': fbx_export, EXPORTERS = {'fbx': fbx_export,
'obj': obj_export, 'obj': obj_export,
'x3d': x3d_export, 'x3d': x3d_export,
@ -369,7 +403,7 @@ def main():
ops["center_boards"] = args.dont_center ops["center_boards"] = args.dont_center
bpy.ops.pcb2blender.import_pcb3d(**ops) bpy.ops.pcb2blender.import_pcb3d(**ops)
# Apply the scene first scene # Apply the scene first scene
c_views = apply_scene(args.scene) c_views = apply_start_scene(args.scene)
c_formats = len(args.format) c_formats = len(args.format)
if c_formats % c_views: if c_formats % c_views:
print("The number of outputs must be a multiple of the views (views: {} outputs: {})".format(c_views, c_formats)) print("The number of outputs must be a multiple of the views (views: {} outputs: {})".format(c_views, c_formats))
@ -378,7 +412,7 @@ def main():
for n in range(c_views): for n in range(c_views):
if n: if n:
# Apply scene N # Apply scene N
apply_scene(args.scene, n) apply_scene(n)
# Get the current slice # Get the current slice
formats = args.format[n*per_pass:(n+1)*per_pass] formats = args.format[n*per_pass:(n+1)*per_pass]
outputs = args.output[n*per_pass:(n+1)*per_pass] outputs = args.output[n*per_pass:(n+1)*per_pass]

View File

@ -13,6 +13,7 @@ Dependencies:
""" """
import json import json
import os import os
import re
from tempfile import NamedTemporaryFile, TemporaryDirectory from tempfile import NamedTemporaryFile, TemporaryDirectory
from .error import KiPlotConfigurationError from .error import KiPlotConfigurationError
from .kiplot import get_output_targets, run_output, run_command, register_xmp_import, config_output, configure_and_run from .kiplot import get_output_targets, run_output, run_command, register_xmp_import, config_output, configure_and_run
@ -26,6 +27,7 @@ from . import log
logger = log.get_logger() logger = log.get_logger()
bb = None bb = None
RE_FILE_ID = re.compile(r"\%\d*d")
def get_board_size(): def get_board_size():
@ -207,10 +209,14 @@ class BlenderPointOfViewOptions(Optionable):
""" *[top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view. """ *[top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
Compatible with `render_3d` """ Compatible with `render_3d` """
self.file_id = '' self.file_id = ''
""" String to diferentiate the name of this view. """ String to diferentiate the name of this point of view.
When empty we use the `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 self._unknown_is_error = True
self._file_id = ''
def config(self, parent): def config(self, parent):
super().config(parent) super().config(parent)
@ -218,9 +224,14 @@ class BlenderPointOfViewOptions(Optionable):
view = self._views.get(self.view, None) view = self._views.get(self.view, None)
if view is not None: if view is not None:
self.view = view self.view = view
self._file_id = self.file_id
if not self._file_id: def get_view(self):
self._file_id = '_'+self._rviews.get(self.view) 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): class PCB3DExportOptions(Base3DOptionsWithHL):
@ -304,6 +315,15 @@ class Blender_ExportOptions(BaseOptions):
""" [dict] Options for the camera. """ [dict] Options for the camera.
If none specified KiBot will create a suitable 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 """ 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 self.render_options = BlenderRenderOptions
""" *[dict] Controls how the render is done for the `render` output type """ """ *[dict] Controls how the render is done for the `render` output type """
self.point_of_view = BlenderPointOfViewOptions self.point_of_view = BlenderPointOfViewOptions
@ -369,7 +389,7 @@ class Blender_ExportOptions(BaseOptions):
elif isinstance(self.point_of_view, BlenderPointOfViewOptions): elif isinstance(self.point_of_view, BlenderPointOfViewOptions):
self.point_of_view = [self.point_of_view] self.point_of_view = [self.point_of_view]
def get_output_filename(self, o, output_dir, pov): def get_output_filename(self, o, output_dir, pov, order):
if o.type == 'render': if o.type == 'render':
self._expand_ext = 'png' self._expand_ext = 'png'
elif o.type == 'blender': elif o.type == 'blender':
@ -377,7 +397,15 @@ class Blender_ExportOptions(BaseOptions):
else: else:
self._expand_ext = o.type self._expand_ext = o.type
cur_id = self._expand_id cur_id = self._expand_id
self._expand_id += pov._file_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(output_dir, o.output) name = self._parent.expand_filename(output_dir, o.output)
self._expand_id = cur_id self._expand_id = cur_id
return name return name
@ -530,6 +558,8 @@ class Blender_ExportOptions(BaseOptions):
if (hasattr(ca, '_pos_x_user_defined') or hasattr(ca, '_pos_y_user_defined') or if (hasattr(ca, '_pos_x_user_defined') or hasattr(ca, '_pos_y_user_defined') or
hasattr(ca, '_pos_z_user_defined')): hasattr(ca, '_pos_z_user_defined')):
scene['camera']['position'] = (ca.pos_x, ca.pos_y, ca.pos_z) scene['camera']['position'] = (ca.pos_x, ca.pos_y, ca.pos_z)
scene['fixed_auto_camera'] = self.fixed_auto_camera
scene['auto_camera_z_axis_factor'] = self.auto_camera_z_axis_factor
ro = self.render_options ro = self.render_options
scene['render'] = {'samples': ro.samples, scene['render'] = {'samples': ro.samples,
'resolution_x': ro.resolution_x, 'resolution_x': ro.resolution_x,
@ -538,11 +568,21 @@ class Blender_ExportOptions(BaseOptions):
'background1': ro.background1, 'background1': ro.background1,
'background2': ro.background2} 'background2': ro.background2}
povs = [] povs = []
last_pov = BlenderPointOfViewOptions()
for pov in self.point_of_view: for pov in self.point_of_view:
povs.append({'rotate_x': -pov.rotate_x, if pov.steps > 1:
'rotate_y': -pov.rotate_y, for _ in range(pov.steps):
'rotate_z': -pov.rotate_z, last_pov.increment(pov)
'view': pov.view}) 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 scene['point_of_view'] = povs
text = json.dumps(scene, sort_keys=True, indent=2) text = json.dumps(scene, sort_keys=True, indent=2)
logger.debug('Scene:\n'+text) logger.debug('Scene:\n'+text)
@ -569,18 +609,22 @@ class Blender_ExportOptions(BaseOptions):
if not pi.stack_boards: if not pi.stack_boards:
cmd.append('--dont_stack_boards') cmd.append('--dont_stack_boards')
cmd.append('--format') cmd.append('--format')
for _ in self.point_of_view: for pov in self.point_of_view:
for o in self.outputs: for _ in range(pov.steps):
cmd.append(o.type) for o in self.outputs:
cmd.append(o.type)
cmd.append('--output') cmd.append('--output')
names = set() names = set()
order = 1
for pov in self.point_of_view: for pov in self.point_of_view:
for o in self.outputs: for _ in range(pov.steps):
name = self.get_output_filename(o, self._parent.output_dir, pov) for o in self.outputs:
if name in names: name = self.get_output_filename(o, self._parent.output_dir, pov, order)
raise KiPlotConfigurationError('Repeated name (use `file_id`): '+name) if name in names:
cmd.append(name) raise KiPlotConfigurationError('Repeated name (use `file_id`): '+name)
names.add(name) cmd.append(name)
names.add(name)
order += 1
cmd.extend(['--scene', f.name]) cmd.extend(['--scene', f.name])
cmd.append(pcb3d_file) cmd.append(pcb3d_file)
# Execute the command # Execute the command

View File

@ -0,0 +1,24 @@
# KiBot Blender export test multiple view points
# Multiple points of view
kibot:
version: 1
outputs:
- name: '3d_video_export'
comment: "Generates the frames for a video of a rotating PCB"
type: blender_export
options:
render_options:
transparent_background: true
samples: 10
fixed_auto_camera: true
auto_camera_z_axis_factor: 1.5
default_file_id: '_%03d'
point_of_view:
- rotate_x: 30
rotate_z: -20
- rotate_x: 4
rotate_z: 4
steps: 90
outputs:
- type: render