[Blender Export] Added Blender render
- Impressive quality thanks to the pcb2blender plug-in
This commit is contained in:
parent
25fd32b84a
commit
eb8c04f870
|
|
@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `vrml` export the 3D model in Virtual Reality Modeling Language (#349)
|
||||
- `ps_sch_print`, `dxf_sch_print` and `hpgl_sch_print` variants of
|
||||
`pdf_sch_print`
|
||||
- `blender_export` exports the PCB to Blender and other 3D formats,
|
||||
renders the PCB with impressive quality (experimental)
|
||||
- New internal filters:
|
||||
- `_only_smd` used to get only SMD parts
|
||||
- `_only_tht` used to get only THT parts
|
||||
|
|
|
|||
97
README.md
97
README.md
|
|
@ -167,6 +167,9 @@ Notes:
|
|||
- Mandatory for `kicost`
|
||||
- Optional to find components costs and specs for `bom`
|
||||
|
||||
[**Blender**](https://www.blender.org/) v3.4.0 [](https://www.blender.org/) [](https://packages.debian.org/bullseye/blender)
|
||||
- Mandatory for `blender_export`
|
||||
|
||||
[**Interactive HTML BoM**](https://github.com/INTI-CMNB/InteractiveHtmlBom) v2.4.1.4 [](https://github.com/INTI-CMNB/InteractiveHtmlBom) 
|
||||
- Mandatory for `ibom`
|
||||
|
||||
|
|
@ -1391,6 +1394,9 @@ The available values for *type* are:
|
|||
- `step` *Standard for the Exchange of Product Data* for the PCB
|
||||
- `vrml` *Virtual Reality Modeling Language* for the PCB
|
||||
- `render_3d` PCB render, from the KiCad's 3D Viewer
|
||||
- `blender_export` PCB export to Blender and high quality 3D render.
|
||||
Including export to: `fbx` (Kaydara's Filmbox), 'obj' (Wavefront), 'x3d' (ISO/IEC standard),
|
||||
`gltf` (GL format), `stl` (3D printing) and 'ply' (Stanford).
|
||||
- Web pages:
|
||||
- `populate` To create step-by-step assembly instructions.
|
||||
- `kikit_present` To create a project presentation web page.
|
||||
|
|
@ -1540,6 +1546,97 @@ Notes:
|
|||
1. Most relevant options are listed first and in **bold**. Which ones are more relevant is quite arbitrary, comments are welcome.
|
||||
2. Aliases are listed in *italics*.
|
||||
|
||||
* Blender Export **Experimental**
|
||||
* Type: `blender_export`
|
||||
* Description: Exports the PCB in various 3D file formats.
|
||||
Also renders the PCB in high-quality.
|
||||
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)
|
||||
* Valid keys:
|
||||
- **`comment`**: [string=''] A comment for documentation purposes. It helps to identify the output.
|
||||
- **`dir`**: [string='./'] Output directory for the generated files.
|
||||
If it starts with `+` the rest is concatenated to the default dir.
|
||||
- **`name`**: [string=''] Used to identify this particular output definition.
|
||||
Avoid using `_` as first character. These names are reserved for KiBot.
|
||||
- **`options`**: [dict] Options for the `blender_export` output.
|
||||
* Valid keys:
|
||||
- **`download`**: [boolean=true] Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD.
|
||||
- **`no_virtual`**: [boolean=false] Used to exclude 3D models for components with 'virtual' attribute.
|
||||
- **`pcb3d`**: [string=''] Name of the output that generated the PCB3D file to import in Belnder.
|
||||
See the `PCB2Blender_2_1` and `PCB2Blender_2_1_haschtl` templates.
|
||||
- **`render_options`**: [dict] How the render is done for the `render` output type.
|
||||
* Valid keys:
|
||||
- **`samples`**: [number=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.
|
||||
- **`transparent_background`**: [boolean=false] Make the background transparent.
|
||||
- `background1`: [string='#66667F'] First color for the background gradient.
|
||||
- `background2`: [string='#CCCCE5'] Second color for the background gradient.
|
||||
- `resolution_x`: [number=1280] Width of the image.
|
||||
- `resolution_y`: [number=720] Height of the image.
|
||||
- **`view`**: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
Compatible with `render_3d`.
|
||||
- `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.
|
||||
- `camera`: [dict] Options for the camera.
|
||||
If none specified KiBot will create a suitable camera.
|
||||
* Valid keys:
|
||||
- `name`: [string=''] Name for the light.
|
||||
- `pos_x`: [number|string] X 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.
|
||||
- `dnf_filter`: [string|list(string)='_none'] Name of the filter to mark components as not fitted.
|
||||
A short-cut to use for simple cases where a variant is an overkill.
|
||||
- `kicad_3d_url`: [string='https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'] Base URL for the KiCad 3D models.
|
||||
- `light`: [dict|list(dict)] Options for the light/s.
|
||||
* Valid keys:
|
||||
- `name`: [string=''] Name for the light.
|
||||
- `pos_x`: [number|string] X 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.
|
||||
- `outputs`: [dict|list(dict)] Outputs to generate in the same run.
|
||||
* Valid keys:
|
||||
- **`type`**: [string='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).
|
||||
- `output`: [string='%f-%i%I%v.%x'] Name for the generated file (%i='blender' %x=VARIABLE).
|
||||
The extension is selected from the type. Affected by global options.
|
||||
- `pcb_import`: Options to configure how Blender imports the PCB.
|
||||
The default values are good for most cases.
|
||||
* Valid keys:
|
||||
- `center`: [boolean=true] Center the PCB at the coordinates origin.
|
||||
- `components`: [boolean=true] Import the components.
|
||||
- `cut_boards`: [boolean=true] Separate the sub-PCBs in separated 3D models.
|
||||
- `enhance_materials`: [boolean=true] Create good looking materials.
|
||||
- `merge_materials`: [boolean=true] Reuse materials.
|
||||
- `solder_joints`: [string='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.
|
||||
- `stack_boards`: [boolean=true] Move the sub-PCBs to their relative position.
|
||||
- `texture_dpi`: [number=1016.0] [508-2032] Texture density in dots per inch.
|
||||
- `pre_transform`: [string|list(string)='_none'] Name of the filter to transform fields before applying other filters.
|
||||
A short-cut to use for simple cases where a variant is an overkill.
|
||||
- `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_z`: [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees].
|
||||
- `variant`: [string=''] Board variant to apply.
|
||||
- `category`: [string|list(string)=''] The category for this output. If not specified an internally defined category is used.
|
||||
Categories looks like file system paths, i.e. **PCB/fabrication/gerber**.
|
||||
The categories are currently used for `navigate_results`.
|
||||
- `disable_run_by_default`: [string|boolean] Use it to disable the `run_by_default` status of other output.
|
||||
Useful when this output extends another and you don't want to generate the original.
|
||||
Use the boolean true value to disable the output you are extending.
|
||||
- `extends`: [string=''] Copy the `options` section from the indicated output.
|
||||
Used to inherit options from another output of the same type.
|
||||
- `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output.
|
||||
- `priority`: [number=50] [0,100] Priority for this output. High priority outputs are created first.
|
||||
Internally we use 10 for low priority, 90 for high priority and 50 for most outputs.
|
||||
- `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested.
|
||||
|
||||
* BoardView
|
||||
* Type: `boardview`
|
||||
* Description: Exports the PCB in board view format.
|
||||
|
|
|
|||
|
|
@ -752,6 +752,9 @@ The available values for *type* are:
|
|||
- `step` *Standard for the Exchange of Product Data* for the PCB
|
||||
- `vrml` *Virtual Reality Modeling Language* for the PCB
|
||||
- `render_3d` PCB render, from the KiCad's 3D Viewer
|
||||
- `blender_export` PCB export to Blender and high quality 3D render.
|
||||
Including export to: `fbx` (Kaydara's Filmbox), 'obj' (Wavefront), 'x3d' (ISO/IEC standard),
|
||||
`gltf` (GL format), `stl` (3D printing) and 'ply' (Stanford).
|
||||
- Web pages:
|
||||
- `populate` To create step-by-step assembly instructions.
|
||||
- `kikit_present` To create a project presentation web page.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,115 @@ preflight:
|
|||
update_xml: true
|
||||
|
||||
outputs:
|
||||
# Blender Export **Experimental**:
|
||||
# Also renders the PCB in high-quality.
|
||||
# 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)
|
||||
- name: 'blender_export_example'
|
||||
comment: 'Exports the PCB in various 3D file formats.'
|
||||
type: 'blender_export'
|
||||
dir: 'Example/blender_export_dir'
|
||||
options:
|
||||
# [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
|
||||
add_default_light: true
|
||||
# [dict] Options for the camera.
|
||||
# If none specified KiBot will create a suitable camera
|
||||
camera:
|
||||
# [string=''] Name for the light
|
||||
name: ''
|
||||
# [number|string] X position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_x: 0
|
||||
# [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_y: 0
|
||||
# [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_z: 0
|
||||
# [string|list(string)='_none'] Name of the filter to mark components as not fitted.
|
||||
# A short-cut to use for simple cases where a variant is an overkill
|
||||
dnf_filter: '_none'
|
||||
# [boolean=true] Downloads missing 3D models from KiCad git. Only applies to models in KISYS3DMOD
|
||||
download: true
|
||||
# [string='https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'] Base URL for the KiCad 3D models
|
||||
kicad_3d_url: 'https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/master/'
|
||||
# [dict|list(dict)] Options for the light/s
|
||||
light:
|
||||
# [string=''] Name for the light
|
||||
- name: ''
|
||||
# [number|string] X position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_x: 0
|
||||
# [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_y: 0
|
||||
# [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions
|
||||
pos_z: 0
|
||||
# [boolean=false] Used to exclude 3D models for components with 'virtual' attribute
|
||||
no_virtual: false
|
||||
# [dict|list(dict)] Outputs to generate in the same run
|
||||
outputs:
|
||||
# [string='%f-%i%I%v.%x'] Name for the generated file (%i='blender' %x=VARIABLE).
|
||||
# The extension is selected from the type. Affected by global options
|
||||
- output: '%f-%i%I%v.%x'
|
||||
# [string='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)
|
||||
type: 'render'
|
||||
# [string=''] Name of the output that generated the PCB3D file to import in Belnder.
|
||||
# See the `PCB2Blender_2_1` and `PCB2Blender_2_1_haschtl` templates
|
||||
pcb3d: ''
|
||||
# Options to configure how Blender imports the PCB.
|
||||
# The default values are good for most cases
|
||||
pcb_import:
|
||||
# [boolean=true] Center the PCB at the coordinates origin
|
||||
center: true
|
||||
# [boolean=true] Import the components
|
||||
components: true
|
||||
# [boolean=true] Separate the sub-PCBs in separated 3D models
|
||||
cut_boards: true
|
||||
# [boolean=true] Create good looking materials
|
||||
enhance_materials: true
|
||||
# [boolean=true] Reuse materials
|
||||
merge_materials: true
|
||||
# [string='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
|
||||
solder_joints: 'SMART'
|
||||
# [boolean=true] Move the sub-PCBs to their relative position
|
||||
stack_boards: true
|
||||
# [number=1016.0] [508-2032] Texture density in dots per inch
|
||||
texture_dpi: 1016.0
|
||||
# [string|list(string)='_none'] Name of the filter to transform fields before applying other filters.
|
||||
# A short-cut to use for simple cases where a variant is an overkill
|
||||
pre_transform: '_none'
|
||||
# [dict] How the render is done for the `render` output type
|
||||
render_options:
|
||||
# [string='#66667F'] First color for the background gradient
|
||||
background1: '#66667F'
|
||||
# [string='#CCCCE5'] Second color for the background gradient
|
||||
background2: '#CCCCE5'
|
||||
# [number=1280] Width of the image
|
||||
resolution_x: 1280
|
||||
# [number=720] Height of the image
|
||||
resolution_y: 720
|
||||
# [number=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
|
||||
samples: 10
|
||||
# [boolean=false] Make the background transparent
|
||||
transparent_background: false
|
||||
# [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]
|
||||
rotate_x: 0
|
||||
# [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees]
|
||||
rotate_y: 0
|
||||
# [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]
|
||||
rotate_z: 0
|
||||
# [string=''] Board variant to apply
|
||||
variant: ''
|
||||
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
# Compatible with `render_3d`
|
||||
view: 'top'
|
||||
# BoardView:
|
||||
# This format allows simple pads and connections navigation, mainly for circuit debug.
|
||||
# The output can be loaded using Open Board View (https://openboardview.org/)
|
||||
|
|
|
|||
|
|
@ -11,17 +11,26 @@
|
|||
#
|
||||
# Should be invoked using:
|
||||
# blender -b --factory-startup -P blender_export.py -- OPTIONS
|
||||
import argparse # to parse options for us and print a nice help message
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys # to get command line args
|
||||
# Blender modules
|
||||
import addon_utils
|
||||
import bpy
|
||||
|
||||
|
||||
X_AXIS = 0
|
||||
Y_AXIS = 1
|
||||
Z_AXIS = 2
|
||||
VALID_FORMATS = {'fbx': 'Filmbox, proprietary format developed by Kaydara (owned by Autodesk)',
|
||||
'obj': 'geometry definition format developed by Wavefront Technologies. Currently open',
|
||||
'x3d': 'VRML successor. A royalty-free ISO/IEC standard for declaratively representing 3D graphics.',
|
||||
'blender': 'Blender native format',
|
||||
'gltf': 'standard file format for three-dimensional scenes and models.',
|
||||
'stl': '3D printing from stereolithography CAD software created by 3D SystemsSTL (only mesh)',
|
||||
'ply': 'Polygon File Format or the Stanford Triangle Format (only mesh)'}
|
||||
'ply': 'Polygon File Format or the Stanford Triangle Format (only mesh)',
|
||||
'render': 'do render'}
|
||||
|
||||
|
||||
def fbx_export(name):
|
||||
|
|
@ -53,19 +62,212 @@ def gltf_export(name):
|
|||
export_draco_mesh_compression_level=6, export_colors=False, export_yup=True)
|
||||
|
||||
|
||||
def render_export(render_path):
|
||||
print('- Render')
|
||||
render = bpy.context.scene.render
|
||||
render.use_file_extension = True
|
||||
render.filepath = render_path
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
def rot(axis, deg):
|
||||
bpy.context.active_object.rotation_euler[axis] = math.radians(deg)
|
||||
|
||||
|
||||
def do_rotate(scene, name, id):
|
||||
rotate = scene.get(name)
|
||||
if rotate:
|
||||
rot(id, rotate)
|
||||
|
||||
|
||||
def do_point_of_view(scene, name):
|
||||
view = scene.get(name)
|
||||
if view is None or view == 'z':
|
||||
return
|
||||
if view == 'Z':
|
||||
rot(Y_AXIS, 180)
|
||||
return
|
||||
if view == 'y':
|
||||
rot(X_AXIS, -90)
|
||||
return
|
||||
if view == 'Y':
|
||||
rot(X_AXIS, 90)
|
||||
rot(Z_AXIS, 180)
|
||||
return
|
||||
if view == 'x':
|
||||
rot(Y_AXIS, 90)
|
||||
rot(Z_AXIS, 90)
|
||||
return
|
||||
if view == 'X':
|
||||
rot(Y_AXIS, -90)
|
||||
rot(Z_AXIS, -90)
|
||||
return
|
||||
|
||||
|
||||
def srgb_to_linearrgb(c):
|
||||
""" Apply the gamma correction """
|
||||
if c < 0:
|
||||
return 0
|
||||
elif c < 0.04045:
|
||||
return c/12.92
|
||||
else:
|
||||
return ((c+0.055)/1.055)**2.4
|
||||
|
||||
|
||||
def hex_to_rgba(hex_value):
|
||||
hex_color = hex_value[1:]
|
||||
r = int(hex_color[:2], base=16)
|
||||
sr = r/255.0
|
||||
lr = srgb_to_linearrgb(sr)
|
||||
g = int(hex_color[2:4], base=16)
|
||||
sg = g/255.0
|
||||
lg = srgb_to_linearrgb(sg)
|
||||
b = int(hex_color[4:6], base=16)
|
||||
sb = b/255.0
|
||||
lb = srgb_to_linearrgb(sb)
|
||||
return (lr, lg, lb, 1.0)
|
||||
|
||||
|
||||
def create_background_gradient(color1, color0):
|
||||
""" This creates a background gradient relative to the camera.
|
||||
I'm not a Blender guru and I took the idea from:
|
||||
https://www.youtube.com/watch?v=9NdZ8leYDcM (in Urdu/Hindi!)
|
||||
If you know a simpler mechanism please let me know """
|
||||
world_name = "World"
|
||||
# Create a "World"
|
||||
scn = bpy.context.scene
|
||||
scn.world = bpy.data.worlds.new(world_name)
|
||||
# Enable the use of nodes
|
||||
scn.world.use_nodes = True
|
||||
# Get the default nodes
|
||||
nt = scn.world.node_tree
|
||||
nodes = nt.nodes
|
||||
# Create a texture coordinate
|
||||
tc = nodes.new(type="ShaderNodeTexCoord")
|
||||
tc.location = (-580, 435)
|
||||
# Create a mapping
|
||||
mp = nodes.new(type="ShaderNodeMapping")
|
||||
mp.location = (-400, 400)
|
||||
mp.inputs["Rotation"].default_value = (0, 0, math.radians(90))
|
||||
mp.inputs["Location"].default_value = (0.7, 0, 0)
|
||||
nt.links.new(tc.outputs["Camera"], mp.inputs["Vector"])
|
||||
# Create a gradient texture
|
||||
gt = nodes.new(type="ShaderNodeTexGradient")
|
||||
gt.location = (-180, 280)
|
||||
gt.gradient_type = "EASING"
|
||||
nt.links.new(mp.outputs["Vector"], gt.inputs["Vector"])
|
||||
# Create a color ramp
|
||||
cr = nodes.new(type="ShaderNodeValToRGB")
|
||||
cr.location = (10, 325)
|
||||
cr.color_ramp.interpolation = "EASE"
|
||||
cr.color_ramp.elements[0].color = hex_to_rgba(color0)
|
||||
cr.color_ramp.elements[1].color = hex_to_rgba(color1)
|
||||
nt.links.new(gt.outputs["Color"], cr.inputs["Fac"])
|
||||
# Move the Background
|
||||
bk1 = nodes["Background"]
|
||||
bk1.location = (355, 100)
|
||||
nt.links.new(cr.outputs["Color"], bk1.inputs["Color"])
|
||||
# Create another background
|
||||
bk2 = nodes.new(type="ShaderNodeBackground")
|
||||
bk2.location = (355, 240)
|
||||
nt.links.new(cr.outputs["Color"], bk2.inputs["Color"])
|
||||
# Create a light path
|
||||
lp = nodes.new(type="ShaderNodeLightPath")
|
||||
lp.location = (355, 580)
|
||||
# Create a mix shader
|
||||
mx = nodes.new(type="ShaderNodeMixShader")
|
||||
mx.location = (560, 220)
|
||||
nt.links.new(lp.outputs["Is Camera Ray"], mx.inputs["Fac"])
|
||||
nt.links.new(bk2.outputs["Background"], mx.inputs[1])
|
||||
nt.links.new(bk1.outputs["Background"], mx.inputs[2])
|
||||
# Move the World Output
|
||||
wo = nodes["World Output"]
|
||||
wo.location = (760, 245)
|
||||
wo_is = wo.inputs["Surface"]
|
||||
nt.links.remove(wo_is.links[0])
|
||||
nt.links.new(mx.outputs["Shader"], wo_is)
|
||||
|
||||
|
||||
def apply_scene(file):
|
||||
# Loads scene
|
||||
if not file:
|
||||
return
|
||||
with open(file, 'rt') as f:
|
||||
text = f.read()
|
||||
print(text)
|
||||
jscene = json.loads(text)
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Select the board
|
||||
print('- Select all')
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
# Apply point of view
|
||||
do_point_of_view(jscene, 'view')
|
||||
# Apply extra rotations
|
||||
do_rotate(jscene, 'rotate_x', 0)
|
||||
do_rotate(jscene, 'rotate_y', 1)
|
||||
do_rotate(jscene, 'rotate_z', 2)
|
||||
|
||||
# Add a camera
|
||||
auto_camera = False
|
||||
camera = jscene.get('camera')
|
||||
if not camera:
|
||||
auto_camera = True
|
||||
camera = {'name': 'kibot_camera', 'position': (0.0, 0.0, 10.0)}
|
||||
name = camera.get('name', 'unknown')
|
||||
pos = camera.get('position', (0, 0, 0))
|
||||
print(f"- Creating camera {name} at {pos}")
|
||||
cam_data = bpy.data.cameras.new(name)
|
||||
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
|
||||
if auto_camera:
|
||||
print('- Changing camera to focus the board')
|
||||
bpy.ops.view3d.camera_to_view_selected()
|
||||
|
||||
# Add lights
|
||||
lights = jscene.get('lights')
|
||||
if lights:
|
||||
for light in lights:
|
||||
name = light.get('name', 'unknown')
|
||||
pos = light.get('position', (0.0, 0.0, 0.0))
|
||||
print(f"- Creating light {name} at {pos}")
|
||||
light_data = bpy.data.lights.new(name, 'POINT')
|
||||
light_ob = bpy.data.objects.new(name=name, object_data=light_data)
|
||||
scene.collection.objects.link(light_ob)
|
||||
light_ob.location = pos
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Setup render options
|
||||
render = jscene.get('render')
|
||||
if render:
|
||||
scene.cycles.samples = render.get('samples', 10)
|
||||
r = scene.render
|
||||
r.engine = 'CYCLES'
|
||||
r.resolution_x = render.get('resolution_x', 1920)
|
||||
r.resolution_y = render.get('resolution_y', 1080)
|
||||
r.resolution_percentage = 100
|
||||
r.use_border = False
|
||||
if render.get('transparent_background'):
|
||||
r.film_transparent = True
|
||||
r.image_settings.color_mode = 'RGBA'
|
||||
else:
|
||||
create_background_gradient(render.get('background1', '#66667F'), render.get('background2', '#CCCCE5'))
|
||||
|
||||
|
||||
EXPORTERS = {'fbx': fbx_export,
|
||||
'obj': obj_export,
|
||||
'x3d': x3d_export,
|
||||
'stl': stl_export,
|
||||
'ply': ply_export,
|
||||
'blender': blender_export,
|
||||
'gltf': gltf_export}
|
||||
'gltf': gltf_export,
|
||||
'render': render_export}
|
||||
|
||||
|
||||
def main():
|
||||
import argparse # to parse options for us and print a nice help message
|
||||
import os
|
||||
import sys # to get command line args
|
||||
# get the args passed to blender after "--", all of which are ignored by
|
||||
# blender so scripts may receive their own arguments
|
||||
argv = sys.argv
|
||||
|
|
@ -95,6 +297,7 @@ def main():
|
|||
help="Rasterized (Cycles) or 3D (deprecated) [RASTERIZED]")
|
||||
parser.add_argument("-M", "--dont_merge_materials", action="store_false", help="do not merge materials")
|
||||
parser.add_argument("-o", "--output", type=str, required=True, nargs='+', help="output file name, can be repeated")
|
||||
parser.add_argument("-r", "--scene", type=str, help="JSON file containing camera, light and render options")
|
||||
parser.add_argument("-s", "--solder_joints", type=str, choices=["NONE", "SMART", "ALL"], default="SMART",
|
||||
help="Add none, all or only for THT/SMD with solder paste [SMART]")
|
||||
parser.add_argument("-S", "--dont_stack_boards", action="store_false", help="do not stack sub-PCBs")
|
||||
|
|
@ -123,6 +326,8 @@ def main():
|
|||
cut_boards=args.dont_cut_boards,
|
||||
stack_boards=args.dont_stack_boards,
|
||||
texture_dpi=args.texture_dpi)
|
||||
# Apply the scene
|
||||
apply_scene(args.scene)
|
||||
# Do all the exports
|
||||
for f, o in zip(args.format, args.output):
|
||||
print(f"Exporting {o} in {f} format")
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ Dependencies:
|
|||
url: https://www.gnu.org/software/bash/
|
||||
debian: bash
|
||||
arch: bash
|
||||
- name: Blender
|
||||
url: https://www.blender.org/
|
||||
debian: blender
|
||||
arch: blender
|
||||
"""
|
||||
import importlib
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ logger = log.get_logger()
|
|||
# Cache to avoid running external many times to check their versions
|
||||
script_versions = {}
|
||||
actions_loaded = False
|
||||
needed_imports = set()
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
|
@ -387,6 +388,17 @@ def config_output(out, dry=False, dont_stop=False):
|
|||
return ok
|
||||
|
||||
|
||||
def get_output_targets(output, parent):
|
||||
out = RegOutput.get_output(output)
|
||||
if out is None:
|
||||
logger.error('Unknown output `{}` selected in {}'.format(output, parent))
|
||||
exit(WRONG_ARGUMENTS)
|
||||
config_output(out)
|
||||
out_dir = get_output_dir(out.dir, out, dry=True)
|
||||
files_list = out.get_targets(out_dir)
|
||||
return files_list, out_dir, out
|
||||
|
||||
|
||||
def run_output(out, dont_stop=False):
|
||||
if out._done:
|
||||
return
|
||||
|
|
@ -820,6 +832,12 @@ def yaml_dump(f, tree):
|
|||
f.write(yaml.dump(tree, sort_keys=False))
|
||||
|
||||
|
||||
def register_xmp_import(name):
|
||||
""" Register an import we need for an example """
|
||||
global needed_imports
|
||||
needed_imports.add(name)
|
||||
|
||||
|
||||
def generate_one_example(dest_dir, types):
|
||||
""" Generate a example config for dest_dir """
|
||||
fname = discover_files(dest_dir)
|
||||
|
|
@ -853,15 +871,8 @@ def generate_one_example(dest_dir, types):
|
|||
yaml_dump(f, {'global': glb})
|
||||
f.write('\n')
|
||||
# A helper for the internal templates
|
||||
needed_imports = []
|
||||
if GS.pcb_file:
|
||||
needed_imports.extend(['Elecrow', 'FusionPCB', 'JLCPCB', 'PCBWay'])
|
||||
if GS.sch_file and GS.pcb_file:
|
||||
needed_imports.append('MacroFab_XYRS')
|
||||
if needed_imports:
|
||||
imports = [{'file': man} for man in needed_imports]
|
||||
yaml_dump(f, {'import': imports})
|
||||
f.write('\n')
|
||||
global needed_imports
|
||||
needed_imports = set()
|
||||
# All the outputs
|
||||
outputs = []
|
||||
for n, cls in OrderedDict(sorted(outs.items())).items():
|
||||
|
|
@ -878,7 +889,13 @@ def generate_one_example(dest_dir, types):
|
|||
if tpls:
|
||||
# Load the templates
|
||||
tpl_names = tpls
|
||||
tpls = [yaml.safe_load(open(t))['outputs'] for t in tpls]
|
||||
tpls = []
|
||||
for t in tpl_names:
|
||||
tree = yaml.safe_load(open(t))
|
||||
tpls.append(tree['outputs'])
|
||||
imps = tree.get('import')
|
||||
if imps:
|
||||
needed_imports.update([imp['file'] for imp in imps])
|
||||
tree = cls.get_conf_examples(n, layers, tpls)
|
||||
if tree:
|
||||
logger.debug('- {}, generated'.format(n))
|
||||
|
|
@ -887,6 +904,10 @@ def generate_one_example(dest_dir, types):
|
|||
outputs.extend(tree)
|
||||
else:
|
||||
logger.debug('- {}, nothing to do'.format(n))
|
||||
if needed_imports:
|
||||
imports = [{'file': man} for man in sorted(needed_imports)]
|
||||
yaml_dump(f, {'import': imports})
|
||||
f.write('\n')
|
||||
if outputs:
|
||||
yaml_dump(f, {'outputs': outputs})
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -91,7 +91,20 @@ class Base3DOptions(VariantOptions):
|
|||
f.write(r.content)
|
||||
return dest
|
||||
|
||||
def download_models(self, rename_filter=None, rename_function=None, rename_data=None):
|
||||
def wrl_name(self, name, force_wrl):
|
||||
""" Try to use the WRL version """
|
||||
if not force_wrl:
|
||||
return name
|
||||
nm, ext = os.path.splitext(name)
|
||||
if ext.lower() == '.wrl':
|
||||
return name
|
||||
nm += '.wrl'
|
||||
if os.path.isfile(nm):
|
||||
logger.debug('- Forcing WRL '+nm)
|
||||
return nm
|
||||
return name
|
||||
|
||||
def download_models(self, rename_filter=None, rename_function=None, rename_data=None, force_wrl=False):
|
||||
""" Check we have the 3D models.
|
||||
Inform missing models.
|
||||
Try to download the missing models
|
||||
|
|
@ -152,7 +165,8 @@ class Base3DOptions(VariantOptions):
|
|||
if replace:
|
||||
source_models.add(replace)
|
||||
old_name = m3d.m_Filename
|
||||
new_name = replace if not is_copy_mode else rename_function(rename_data, replace)
|
||||
new_name = (self.wrl_name(replace, force_wrl) if not is_copy_mode else
|
||||
rename_function(rename_data, replace))
|
||||
self.undo_3d_models[new_name] = old_name
|
||||
m3d.m_Filename = new_name
|
||||
models_replaced = True
|
||||
|
|
@ -164,7 +178,8 @@ class Base3DOptions(VariantOptions):
|
|||
# This is completely valid for KiCad, but kicad2step doesn't support it
|
||||
source_models.add(full_name)
|
||||
old_name = m3d.m_Filename
|
||||
new_name = full_name if not is_copy_mode else rename_function(rename_data, full_name)
|
||||
new_name = (self.wrl_name(full_name, force_wrl) if not is_copy_mode else
|
||||
rename_function(rename_data, full_name))
|
||||
self.undo_3d_models[new_name] = old_name
|
||||
m3d.m_Filename = new_name
|
||||
if not models_replaced and extra_debug:
|
||||
|
|
@ -191,10 +206,10 @@ class Base3DOptions(VariantOptions):
|
|||
models.add(full_name)
|
||||
return list(models)
|
||||
|
||||
def filter_components(self, highlight=None):
|
||||
def filter_components(self, highlight=None, force_wrl=False):
|
||||
if not self._comps:
|
||||
# No variant/filter to apply
|
||||
if self.download_models():
|
||||
if self.download_models(force_wrl=force_wrl):
|
||||
# Some missing components found and we downloaded them
|
||||
# Save the fixed board
|
||||
ret = self.save_tmp_board()
|
||||
|
|
@ -203,7 +218,7 @@ class Base3DOptions(VariantOptions):
|
|||
return ret
|
||||
return GS.pcb_file
|
||||
self.filter_pcb_components(do_3D=True, do_2D=True, highlight=highlight)
|
||||
self.download_models()
|
||||
self.download_models(force_wrl=force_wrl)
|
||||
fname = self.save_tmp_board()
|
||||
self.unfilter_pcb_components(do_3D=True, do_2D=True)
|
||||
return fname
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
# -*- 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 copy import copy
|
||||
import json
|
||||
import os
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .error import KiPlotConfigurationError
|
||||
from .kiplot import get_output_targets, run_output, run_command, register_xmp_import
|
||||
from .gs import GS
|
||||
from .optionable import Optionable
|
||||
from .out_base_3d import Base3DOptions, Base3D
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from . import log
|
||||
|
||||
logger = log.get_logger()
|
||||
bb = None
|
||||
|
||||
|
||||
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, field=None):
|
||||
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._unkown_is_error = True
|
||||
|
||||
|
||||
class BlenderOutputOptions(Optionable):
|
||||
""" What is generated """
|
||||
def __init__(self, field=None):
|
||||
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='blender' %x=VARIABLE).
|
||||
The extension is selected from the type """
|
||||
self._unkown_is_error = True
|
||||
|
||||
|
||||
class BlenderLightOptions(Optionable):
|
||||
""" A light in the scene. Currently also for the camera """
|
||||
def __init__(self, field=None):
|
||||
super().__init__()
|
||||
with document:
|
||||
self.name = ""
|
||||
""" Name for the light """
|
||||
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._unkown_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 BlenderRenderOptions(Optionable):
|
||||
""" Render parameters """
|
||||
def __init__(self, field=None):
|
||||
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.resolution_y = 720
|
||||
""" Height of the image """
|
||||
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._unkown_is_error = True
|
||||
|
||||
|
||||
class Blender_ExportOptions(Base3DOptions):
|
||||
_views = {'top': 'z', 'bottom': 'Z', 'front': 'y', 'rear': 'Y', 'right': 'x', 'left': 'X'}
|
||||
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.pcb3d = ""
|
||||
""" *Name of the output that generated the PCB3D file to import in Belnder.
|
||||
See the `PCB2Blender_2_1` 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 = BlenderLightOptions
|
||||
""" [dict] Options for the camera.
|
||||
If none specified KiBot will create a suitable camera """
|
||||
self.render_options = BlenderRenderOptions
|
||||
""" *[dict] How the render is done for the `render` output type """
|
||||
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` """
|
||||
super().__init__()
|
||||
self._expand_id += '_blender'
|
||||
self._unkown_is_error = True
|
||||
|
||||
def config(self, parent):
|
||||
super().config(parent)
|
||||
# Check we at least have a name for the source output
|
||||
if not self.pcb3d:
|
||||
raise KiPlotConfigurationError('You must specify the name of the output that generates the PCB3D file')
|
||||
# Do we have outputs?
|
||||
if isinstance(self.outputs, type):
|
||||
raise KiPlotConfigurationError('You must specify at least one output')
|
||||
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'
|
||||
_, _, size = get_board_size()
|
||||
light.pos_x = -size*3.33
|
||||
light.pos_y = size*3.33
|
||||
light.pos_z = size*5.0
|
||||
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()
|
||||
# View point
|
||||
view = self._views.get(self.view, None)
|
||||
if view is not None:
|
||||
self.view = view
|
||||
|
||||
def run(self, output):
|
||||
super().run(output)
|
||||
command = self.ensure_tool('Blender')
|
||||
pcb3d_targets, pcb3d_out_dir, 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)
|
||||
if not os.path.isfile(pcb3d_file):
|
||||
raise KiPlotConfigurationError('Missing '+pcb3d_file)
|
||||
# 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)} for light in self.light]
|
||||
scene['lights'] = lights
|
||||
if self.camera:
|
||||
scene['camera'] = {'name': self.camera.name,
|
||||
'position': (self.camera.pos_x, self.camera.pos_y, self.camera.pos_z)}
|
||||
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}
|
||||
if self.rotate_x:
|
||||
scene['rotate_x'] = -self.rotate_x
|
||||
if self.rotate_y:
|
||||
scene['rotate_y'] = -self.rotate_y
|
||||
if self.rotate_z:
|
||||
scene['rotate_z'] = -self.rotate_z
|
||||
if self.view:
|
||||
scene['view'] = self.view
|
||||
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')
|
||||
cmd.append('--format')
|
||||
cmd.extend([o.type for o in self.outputs])
|
||||
cmd.append('--output')
|
||||
for o in self.outputs:
|
||||
if o.type == 'render':
|
||||
self._expand_ext = 'png'
|
||||
elif o.type == 'blender':
|
||||
self._expand_ext = 'blend'
|
||||
else:
|
||||
self._expand_ext = o.type
|
||||
cmd.append(self._parent.expand_filename(self._parent.output_dir, o.output))
|
||||
cmd.extend(['--scene', f.name])
|
||||
cmd.append(pcb3d_file)
|
||||
# Execute the command
|
||||
run_command(cmd)
|
||||
|
||||
|
||||
@output_class
|
||||
class Blender_Export(Base3D):
|
||||
""" Blender Export **Experimental**
|
||||
Exports the PCB in various 3D file formats.
|
||||
Also renders the PCB in high-quality.
|
||||
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) """
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with document:
|
||||
self.options = Blender_ExportOptions
|
||||
""" *[dict] Options for the `blender_export` output """
|
||||
self._category = 'PCB/3D'
|
||||
|
||||
@staticmethod
|
||||
def get_conf_examples(name, layers, templates):
|
||||
if not GS.check_tool(name, 'Blender'):
|
||||
return None
|
||||
outs = []
|
||||
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')
|
||||
out_ops = {'pcb3d': '_PCB2Blender_2_1', 'outputs': [{'type': 'render'}]}
|
||||
if has_top:
|
||||
gb = {}
|
||||
gb['name'] = 'basic_{}_top'.format(name)
|
||||
gb['comment'] = '3D view from top (Blender)'
|
||||
gb['type'] = name
|
||||
gb['dir'] = '3D'
|
||||
gb['options'] = copy(out_ops)
|
||||
outs.append(gb)
|
||||
gb = {}
|
||||
gb['name'] = 'basic_{}_30deg'.format(name)
|
||||
gb['comment'] = '3D view from 30 degrees (Blender)'
|
||||
gb['type'] = name
|
||||
gb['dir'] = '3D'
|
||||
gb['output_id'] = '30deg'
|
||||
gb['options'] = copy(out_ops)
|
||||
gb['options'].update({'rotate_x': 30, 'rotate_z': -20})
|
||||
outs.append(gb)
|
||||
if has_bottom:
|
||||
gb = {}
|
||||
gb['name'] = 'basic_{}_bottom'.format(name)
|
||||
gb['comment'] = '3D view from bottom (Blender)'
|
||||
gb['type'] = name
|
||||
gb['dir'] = '3D'
|
||||
gb['options'] = copy(out_ops)
|
||||
gb['options'].update({'view': 'bottom'})
|
||||
outs.append(gb)
|
||||
return outs
|
||||
|
|
@ -24,8 +24,8 @@ from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED, ZIP_BZIP2, ZIP_LZMA
|
|||
from tarfile import open as tar_open
|
||||
from collections import OrderedDict
|
||||
from .gs import GS
|
||||
from .kiplot import config_output, get_output_dir, run_output
|
||||
from .misc import WRONG_INSTALL, W_EMPTYZIP, WRONG_ARGUMENTS, INTERNAL_ERROR
|
||||
from .kiplot import config_output, run_output, get_output_targets
|
||||
from .misc import WRONG_INSTALL, W_EMPTYZIP, INTERNAL_ERROR
|
||||
from .optionable import Optionable, BaseOptions
|
||||
from .registrable import RegOutput
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
|
|
@ -153,13 +153,8 @@ class CompressOptions(BaseOptions):
|
|||
output_out_dir = None
|
||||
if f.from_output:
|
||||
logger.debugl(2, '- From output `{}`'.format(f.from_output))
|
||||
out = RegOutput.get_output(f.from_output)
|
||||
if out is None:
|
||||
logger.error('Unknown output `{}` selected in {}'.format(f.from_output, self._parent))
|
||||
exit(WRONG_ARGUMENTS)
|
||||
config_output(out)
|
||||
output_out_dir = out_dir = get_output_dir(out.dir, out, dry=True)
|
||||
files_list = out.get_targets(out_dir)
|
||||
files_list, out_dir, out = get_output_targets(f.from_output, self._parent)
|
||||
output_out_dir = out_dir
|
||||
logger.debugl(2, '- List of files: {}'.format(files_list))
|
||||
if out_dir not in dirs_list:
|
||||
dirs_list.append(out_dir)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ from . import log
|
|||
logger = log.get_logger()
|
||||
|
||||
|
||||
def replace_ext(file, ext):
|
||||
file, ext = os.path.splitext(file)
|
||||
return file+'.wrl'
|
||||
|
||||
|
||||
class VRMLOptions(Base3DOptions):
|
||||
def __init__(self):
|
||||
with document:
|
||||
|
|
@ -48,7 +53,7 @@ class VRMLOptions(Base3DOptions):
|
|||
if self.dir_models:
|
||||
# We will also generate the models
|
||||
dir = os.path.join(out_dir, self.dir_models)
|
||||
filtered = {os.path.join(dir, os.path.basename(m)) for m in self.list_models() if m.endswith('.wrl')}
|
||||
filtered = {os.path.join(dir, os.path.basename(replace_ext(m, 'wrl'))) for m in self.list_models()}
|
||||
targets.extend(list(filtered))
|
||||
return targets
|
||||
|
||||
|
|
@ -59,7 +64,7 @@ class VRMLOptions(Base3DOptions):
|
|||
def run(self, name):
|
||||
command = self.ensure_tool('KiAuto')
|
||||
super().run(name)
|
||||
board_name = self.filter_components()
|
||||
board_name = self.filter_components(force_wrl=True)
|
||||
cmd = [command, 'export_vrml', '--output_name', os.path.basename(name), '-U', self.model_units]
|
||||
if self.dir_models:
|
||||
cmd.extend(['--dir_models', self.dir_models])
|
||||
|
|
|
|||
|
|
@ -66,6 +66,43 @@ deps = '{\
|
|||
"url": "https://www.gnu.org/software/bash/",\
|
||||
"url_down": null\
|
||||
},\
|
||||
"Blender": {\
|
||||
"arch": "blender",\
|
||||
"command": "blender",\
|
||||
"comments": [],\
|
||||
"deb_package": "blender",\
|
||||
"downloader": null,\
|
||||
"downloader_str": null,\
|
||||
"extra_arch": null,\
|
||||
"extra_deb": null,\
|
||||
"help_option": "--version",\
|
||||
"importance": 10000,\
|
||||
"in_debian": true,\
|
||||
"is_kicad_plugin": false,\
|
||||
"is_python": false,\
|
||||
"name": "Blender",\
|
||||
"no_cmd_line_version": false,\
|
||||
"no_cmd_line_version_old": false,\
|
||||
"output": "blender_export",\
|
||||
"plugin_dirs": null,\
|
||||
"pypi_name": "Blender",\
|
||||
"roles": [\
|
||||
{\
|
||||
"desc": null,\
|
||||
"mandatory": true,\
|
||||
"max_version": null,\
|
||||
"output": "blender_export",\
|
||||
"version": [\
|
||||
3,\
|
||||
4,\
|
||||
0\
|
||||
]\
|
||||
}\
|
||||
],\
|
||||
"tests": [],\
|
||||
"url": "https://www.blender.org/",\
|
||||
"url_down": null\
|
||||
},\
|
||||
"Colorama": {\
|
||||
"arch": "python-colorama",\
|
||||
"command": "colorama",\
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# KiBot Blender export test 1
|
||||
kibot:
|
||||
version: 1
|
||||
|
||||
import:
|
||||
- file: PCB2Blender_2_1
|
||||
|
||||
outputs:
|
||||
- name: '3d_export'
|
||||
comment: "Exports the PCB in blender format"
|
||||
type: blender_export
|
||||
disable_run_by_default: _PCB2Blender_2_1
|
||||
options:
|
||||
pcb3d: _PCB2Blender_2_1
|
||||
rotate_x: 30
|
||||
rotate_z: -20
|
||||
# view: bottom
|
||||
# camera:
|
||||
# name: MyCamera
|
||||
# pos_x: 0.3
|
||||
# pos_y: width*2
|
||||
# pos_z: size*3
|
||||
render_options:
|
||||
transparent_background: true
|
||||
samples: 10
|
||||
#resolution_x: 1920
|
||||
#resolution_y: 1080
|
||||
outputs:
|
||||
- type: blender
|
||||
- type: render
|
||||
Loading…
Reference in New Issue