[Blender Export][Added] Options useful to generate simple animations
- The resulting PNGs can be converted to MP4 using ffmpeg
This commit is contained in:
parent
4202f01c01
commit
948a40fb91
|
|
@ -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
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue