From e16086ca702bb28b69245364cbffc3d505cdb518 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Thu, 26 Jan 2023 18:13:38 -0300 Subject: [PATCH] [Blender Export] Now can be used as renderer - Tested with kikit_present and populate - Also added `auto_crop` --- README.md | 5 ++ docs/samples/generic_plot.kibot.yaml | 5 ++ kibot/misc.py | 2 +- kibot/optionable.py | 32 -------- kibot/out_base.py | 26 +++++++ kibot/out_blender_export.py | 77 ++++++++++++++++++- kibot/out_kikit_present.py | 23 +++--- kibot/out_pcbdraw.py | 31 +++++++- kibot/out_populate.py | 34 +++----- kibot/out_render_3d.py | 24 ++++++ src/kibot-check | 11 ++- .../kikit_present_local_1.kibot.yaml | 15 +++- 12 files changed, 209 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index ab35110b..891bf637 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ Notes: [**ImageMagick**](https://imagemagick.org/) [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://imagemagick.org/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/imagemagick) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png) - Optional to: + - Automatically crop images for `blender_export` - Create outputs preview for `navigate_results` - Create monochrome prints and scaled PNG files for `pcb_print` - Create JPG and BMP images for `pcbdraw` @@ -1608,10 +1609,14 @@ Notes: - **`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. + - `auto_crop`: [boolean=false] When enabled the image will be post-processed to remove the empty space around the image. + In this mode the `background2` is changed to be the same as `background1`. - `background1`: [string='#66667F'] First color for the background gradient. - `background2`: [string='#CCCCE5'] Second color for the background gradient. + - *height*: Alias for resolution_y. - `resolution_x`: [number=1280] Width of the image. - `resolution_y`: [number=720] Height of the image. + - *width*: Alias for resolution_x. - `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. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 61d7aa23..1f3d28f9 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -209,10 +209,14 @@ outputs: view: 'top' # [dict] Controls how the render is done for the `render` output type render_options: + # [boolean=false] When enabled the image will be post-processed to remove the empty space around the image. + # In this mode the `background2` is changed to be the same as `background1` + auto_crop: false # [string='#66667F'] First color for the background gradient background1: '#66667F' # [string='#CCCCE5'] Second color for the background gradient background2: '#CCCCE5' + # `height` is an alias for `resolution_y` # [number=1280] Width of the image resolution_x: 1280 # [number=720] Height of the image @@ -222,6 +226,7 @@ outputs: samples: 10 # [boolean=false] Make the background transparent transparent_background: false + # `width` is an alias for `resolution_x` # 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/) diff --git a/kibot/misc.py b/kibot/misc.py index 92ec83c3..b1c47962 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -271,7 +271,7 @@ KICAD5_SVG_SCALE = 116930/297002200 USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0' # Text used to disable 3D models DISABLE_3D_MODEL_TEXT = '_Disabled_by_KiBot' -RENDERERS = ['pcbdraw', 'render_3d'] +RENDERERS = ['pcbdraw', 'render_3d', 'blender_export'] PCB_GENERATORS = ['pcb_variant', 'panelize'] KIKIT_UNIT_ALIASES = {'millimeters': 'mm', 'inches': 'inch', 'mils': 'mil'} diff --git a/kibot/optionable.py b/kibot/optionable.py index 741c0ca4..3c0b3e8d 100644 --- a/kibot/optionable.py +++ b/kibot/optionable.py @@ -468,38 +468,6 @@ class Optionable(object): def color_str_to_rgb(self, color): return self.color_to_rgb(self.parse_one_color(color)) - def save_renderer_options(self): - """ Save the current renderer settings """ - options = self._renderer.options - self.old_filters_to_expand = options._filters_to_expand - self.old_show_components = options.show_components - self.old_highlight = options.highlight - self.old_output = options.output - self.old_dir = self._renderer.dir - self.old_done = self._renderer._done - if self._renderer_is_pcbdraw: - self.old_bottom = options.bottom - self.old_add_to_variant = options.add_to_variant - else: # render_3D - self.old_view = options.view - self.old_show_all_components = options._show_all_components - - def restore_renderer_options(self): - """ Restore the renderer settings """ - options = self._renderer.options - options._filters_to_expand = self.old_filters_to_expand - options.show_components = self.old_show_components - options.highlight = self.old_highlight - options.output = self.old_output - self._renderer.dir = self.old_dir - self._renderer._done = self.old_done - if self._renderer_is_pcbdraw: - options.bottom = self.old_bottom - options.add_to_variant = self.old_add_to_variant - else: # render_3D - options.view = self.old_view - options._show_all_components = self.old_show_all_components - class BaseOptions(Optionable): """ A class to validate and hold output options. diff --git a/kibot/out_base.py b/kibot/out_base.py index 3b1c0a94..9be07893 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -990,6 +990,32 @@ class VariantOptions(BaseOptions): self._sub_pcb = self.variant._sub_pcb self._comps = comps + # The following 3 members are used by 2D and 3D renderers + def setup_renderer(self, components, active_components): + """ Setup the options to use it as a renderer """ + self._show_all_components = False + self._filters_to_expand = False + self.highlight = self.solve_kf_filters([c for c in active_components if c]) + self.show_components = [c for c in components if c] + if self.show_components: + self.show_components = self.solve_kf_filters(self.show_components) + + def save_renderer_options(self): + """ Save the current renderer settings """ + self.old_filters_to_expand = self._filters_to_expand + self.old_show_components = self.show_components + self.old_highlight = self.highlight + self.old_dir = self._parent.dir + self.old_done = self._parent._done + + def restore_renderer_options(self): + """ Restore the renderer settings """ + self._filters_to_expand = self.old_filters_to_expand + self.show_components = self.old_show_components + self.highlight = self.old_highlight + self._parent.dir = self.old_dir + self._parent._done = self.old_done + class PcbMargin(Optionable): """ To adjust each margin """ diff --git a/kibot/out_blender_export.py b/kibot/out_blender_export.py index 9312ca90..e32a5c75 100644 --- a/kibot/out_blender_export.py +++ b/kibot/out_blender_export.py @@ -8,6 +8,8 @@ Dependencies: - from: Blender role: mandatory version: 3.4.0 + - from: ImageMagick + role: Automatically crop images """ import json import os @@ -122,14 +124,21 @@ class BlenderRenderOptions(Optionable): Use 1 for a raw preview, 10 for a draft and 100 or more for the final render """ self.resolution_x = 1280 """ Width of the image """ + self.width = None + """ {resolution_x} """ self.resolution_y = 720 """ Height of the image """ + self.height = None + """ {resolution_y} """ self.transparent_background = False """ *Make the background transparent """ self.background1 = "#66667F" """ First color for the background gradient """ self.background2 = "#CCCCE5" """ Second color for the background gradient """ + self.auto_crop = False + """ When enabled the image will be post-processed to remove the empty space around the image. + In this mode the `background2` is changed to be the same as `background1` """ self._unkown_is_error = True @@ -154,6 +163,7 @@ class BlenderPointOfViewOptions(Optionable): """ String to diferentiate the name of this view. When empty we use the `view` """ self._unkown_is_error = True + self._file_id = '' def config(self, parent): super().config(parent) @@ -190,6 +200,38 @@ class PCB3DExportOptions(Base3DOptionsWithHL): p._expand_ext = cur_ext return out_name + def setup_renderer(self, components, active_components, bottom, name): + super().setup_renderer(components, active_components) + self._pov.view = 'Z' if bottom else 'z' + # Expand the name using .PNG + cur_ext = self._expand_ext + self._expand_ext = 'png' + o_name = self.expand_filename_both(name, is_sch=False) + self._expand_ext = cur_ext + self._out.output = o_name + return o_name + + def save_renderer_options(self): + """ Save the current renderer settings """ + p = self._parent + # We are an option inside another option + self._parent = self._parent._parent + super().save_renderer_options() + self._parent = p + self.old_show_all_components = self._show_all_components + self.old_view = self._pov.view + self.old_output = self._out.output + + def restore_renderer_options(self): + """ Restore the renderer settings """ + p = self._parent + self._parent = self._parent._parent + super().restore_renderer_options() + self._parent = p + self._show_all_components = self.old_show_all_components + self._pov.view = self.old_view + self._out.output = self.old_output + class Blender_ExportOptions(BaseOptions): def __init__(self): @@ -222,9 +264,12 @@ class Blender_ExportOptions(BaseOptions): def config(self, parent): super().config(parent) # Check we at least have a name for the source output - if isinstance(self.pcb3d, type) or (isinstance(self.pcb3d, str) and not self.pcb3d): + if isinstance(self.pcb3d, str) and not self.pcb3d: raise KiPlotConfigurationError('You must specify the name of the output that' ' generates the PCB3D file or its options') + if isinstance(self.pcb3d, type): + self.pcb3d = PCB3DExportOptions() + self.pcb3d.config(self) # Do we have outputs? if isinstance(self.outputs, type): self.outputs = [] @@ -409,6 +454,10 @@ class Blender_ExportOptions(BaseOptions): return # Make sure Blender is available command = self._pcb3d.ensure_tool('Blender') + if self.render_options.auto_crop: + # Avoid a gradient + self.render_options.background2 = self.render_options.background1 + convert_command = self.ensure_tool('ImageMagick') # Create a JSON with the scene information with NamedTemporaryFile(mode='w', suffix='.json') as f: scene = {} @@ -473,6 +522,13 @@ class Blender_ExportOptions(BaseOptions): cmd.append(pcb3d_file) # Execute the command run_command(cmd) + if self.render_options.auto_crop: + for pov in self.point_of_view: + for o in self.outputs: + if o.type != 'render': + continue + name = self.get_output_filename(o, self._parent.output_dir, pov) + run_command([convert_command, name, '-trim', '+repage', '-trim', '+repage', name]) @output_class @@ -501,6 +557,25 @@ class Blender_Export(Base3D): files.extend(self.options.pcb3d.list_models()) return files + def get_renderer_options(self): + """ Where are the options for this output when used as a 'renderer' """ + ops = self.options + out = next(filter(lambda x: x.type == 'render', ops.outputs), None) + res = None + if out is not None: + if isinstance(ops.pcb3d, str): + # We can't configure it + out = None + else: + res = ops.pcb3d + res._pov = ops.point_of_view[0] + res._out = out + return res if out is not None else None + + def get_extension(self): + # Used when we are a renderer + return 'png' + @staticmethod def get_conf_examples(name, layers, templates): if not GS.check_tool(name, 'Blender'): diff --git a/kibot/out_kikit_present.py b/kibot/out_kikit_present.py index b71feae2..3d1503aa 100644 --- a/kibot/out_kikit_present.py +++ b/kibot/out_kikit_present.py @@ -122,23 +122,19 @@ class PresentBoards(Optionable): return self.name, self.comment, self.pcb_file, self.front_image, self.back_image, self.gerbers def generate_image(self, back, tmp_name): - self.save_renderer_options() - options = self._renderer.options + options = self._renderer.get_renderer_options() + if options is None: + raise KiPlotConfigurationError('No suitable renderer ({})'.format(self._renderer)) + # Memorize the current options + options.save_renderer_options() logger.debug('Starting renderer with back: {}, name: {}'.format(back, tmp_name)) # Configure it according to our needs - options._filters_to_expand = False - options.show_components = None if self._renderer_is_pcbdraw else [] - options.highlight = [] - options.output = tmp_name + options.setup_renderer([], [], back, tmp_name) + self._renderer.dir = self._parent._parent.dir self._renderer._done = False - if self._renderer_is_pcbdraw: - options.add_to_variant = False - options.bottom = back - else: # render_3D - options.view = 'Z' if back else 'z' - options._show_all_components = False run_output(self._renderer) - self.restore_renderer_options() + # Restore the options + options.restore_renderer_options() def do_compress(self, tmp_name, out): tree = {'name': '_temporal_compress_gerbers', @@ -202,7 +198,6 @@ class PresentBoards(Optionable): format(out, RENDERERS)) config_output(out) self._renderer = out - self._renderer_is_pcbdraw = out.type == 'pcbdraw' tmp_name = _get_tmp_name(out.get_extension()) self.temporals.append(tmp_name) self.generate_image(back, tmp_name) diff --git a/kibot/out_pcbdraw.py b/kibot/out_pcbdraw.py index 8ada7015..10662972 100644 --- a/kibot/out_pcbdraw.py +++ b/kibot/out_pcbdraw.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022 Salvador E. Tropea -# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2023 Salvador E. Tropea +# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) """ @@ -322,6 +322,29 @@ class PcbDrawOptions(VariantOptions): self._expand_id = 'bottom' if self.bottom else 'top' self._expand_ext = self.format + def setup_renderer(self, components, active_components, bottom, name): + super().setup_renderer(components, active_components) + self.add_to_variant = False + self.bottom = bottom + self.output = name + if not self.show_components: + self.show_components = None + return self.expand_filename_both(name, is_sch=False) + + def save_renderer_options(self): + """ Save the current renderer settings """ + super().save_renderer_options() + self.old_bottom = self.bottom + self.old_add_to_variant = self.add_to_variant + self.old_output = self.output + + def restore_renderer_options(self): + """ Restore the renderer settings """ + super().restore_renderer_options() + self.bottom = self.old_bottom + self.add_to_variant = self.old_add_to_variant + self.output = self.old_output + def expand_filtered_components(self, components): """ Expands references to filters in show_components """ if not components or not self._filters_to_expand: @@ -530,6 +553,10 @@ class PcbDraw(BaseOutput): # noqa: F821 files.append(self.options.style) return files + def get_renderer_options(self): + """ Where are the options for this output when used as a 'renderer' """ + return self.options + @staticmethod def get_conf_examples(name, layers, templates): outs = [] diff --git a/kibot/out_populate.py b/kibot/out_populate.py index 661bc8b4..324807f8 100644 --- a/kibot/out_populate.py +++ b/kibot/out_populate.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 Salvador E. Tropea -# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2022-2023 Salvador E. Tropea +# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) """ @@ -82,33 +82,22 @@ class PopulateOptions(VariantOptions): img_dir = os.path.dirname(self._parent.expand_filename(out_dir, self.imgname)) return [self._parent.expand_filename(out_dir, self.get_out_file_name()), img_dir] - def generate_image(self, side, components, active_components, name): - options = self._renderer.options + def generate_image(self, side, components, active_components, name, options): logger.debug('Starting renderer with side: {}, components: {}, high: {}, image: {}'. format(side, components, active_components, name)) # Configure it according to our needs - options._filters_to_expand = False - options.show_components = [c for c in components if c] - if not options.show_components: - options.show_components = None if self._renderer_is_pcbdraw else [] - else: - options.show_components = options.solve_kf_filters(options.show_components) - options.highlight = options.solve_kf_filters([c for c in active_components if c]) - options.output = name + o_name = options.setup_renderer(components, active_components, side.startswith("back"), name) self._renderer.dir = self._parent.dir self._renderer._done = False - if self._renderer_is_pcbdraw: - options.add_to_variant = False - options.bottom = side.startswith("back") - else: # render_3D - options.view = 'Z' if side.startswith("back") else 'z' - options._show_all_components = False run_output(self._renderer) - return options.expand_filename_both(name, is_sch=False) + return o_name def generate_images(self, dir_name, content): + options = self._renderer.get_renderer_options() + if options is None: + raise KiPlotConfigurationError('No suitable renderer ({})'.format(self._renderer)) # Memorize the current options - self.save_renderer_options() + options.save_renderer_options() dir = os.path.dirname(os.path.join(dir_name, self.imgname)) if not os.path.exists(dir): os.makedirs(dir) @@ -119,9 +108,9 @@ class PopulateOptions(VariantOptions): for x in item["steps"]: counter += 1 filename = self.imgname.replace('%d', str(counter)) - x["img"] = self.generate_image(x["side"], x["components"], x["active_components"], filename) + x["img"] = self.generate_image(x["side"], x["components"], x["active_components"], filename, options) # Restore the options - self.restore_renderer_options() + options.restore_renderer_options() return content def run(self, dir_name): @@ -140,7 +129,6 @@ class PopulateOptions(VariantOptions): if out.type not in RENDERERS: raise KiPlotConfigurationError('The `renderer` must be {} type, not {}'.format(RENDERERS, out.type)) self._renderer = out - self._renderer_is_pcbdraw = out.type == 'pcbdraw' # Load the input content try: _, content = load_content(self.input) diff --git a/kibot/out_render_3d.py b/kibot/out_render_3d.py index d639d782..7e028436 100644 --- a/kibot/out_render_3d.py +++ b/kibot/out_render_3d.py @@ -176,6 +176,26 @@ class Render3DOptions(Base3DOptionsWithHL): self.view = view self._expand_id += '_'+self._rviews.get(self.view) + def setup_renderer(self, components, active_components, bottom, name): + super().setup_renderer(components, active_components) + self.view = 'Z' if bottom else 'z' + self.output = name + return self.expand_filename_both(name, is_sch=False) + + def save_renderer_options(self): + """ Save the current renderer settings """ + super().save_renderer_options() + self.old_show_all_components = self._show_all_components + self.old_view = self.view + self.old_output = self.output + + def restore_renderer_options(self): + """ Restore the renderer settings """ + super().restore_renderer_options() + self._show_all_components = self.old_show_all_components + self.view = self.old_view + self.output = self.old_output + def add_step(self, cmd, steps, ops): if steps: cmd.extend([ops, str(steps)]) @@ -265,6 +285,10 @@ class Render_3D(Base3D): # noqa: F821 """ *[dict] Options for the `render_3d` output """ self._category = 'PCB/3D' + def get_renderer_options(self): + """ Where are the options for this output when used as a 'renderer' """ + return self.options + @staticmethod def get_conf_examples(name, layers, templates): outs = [] diff --git a/src/kibot-check b/src/kibot-check index e40eb77f..3f00b36e 100755 --- a/src/kibot-check +++ b/src/kibot-check @@ -250,17 +250,24 @@ deps = '{\ ],\ "extra_deb": null,\ "help_option": "--version",\ - "importance": 4,\ + "importance": 5,\ "in_debian": true,\ "is_kicad_plugin": false,\ "is_python": false,\ "name": "ImageMagick",\ "no_cmd_line_version": false,\ "no_cmd_line_version_old": false,\ - "output": "navigate_results",\ + "output": "blender_export",\ "plugin_dirs": null,\ "pypi_name": "ImageMagick",\ "roles": [\ + {\ + "desc": "Automatically crop images",\ + "mandatory": false,\ + "max_version": null,\ + "output": "blender_export",\ + "version": null\ + },\ {\ "desc": "Create outputs preview",\ "mandatory": false,\ diff --git a/tests/yaml_samples/kikit_present_local_1.kibot.yaml b/tests/yaml_samples/kikit_present_local_1.kibot.yaml index 3cb2d933..67052097 100644 --- a/tests/yaml_samples/kikit_present_local_1.kibot.yaml +++ b/tests/yaml_samples/kikit_present_local_1.kibot.yaml @@ -13,7 +13,8 @@ outputs: mode: local comment: This is a comment name: Light control - back_image: Raytraced + # back_image: Raytraced + back_image: Blender repository: 'https://github.com/INTI-CMNB/KiBot/' - name: PcbDraw @@ -37,6 +38,18 @@ outputs: rotate_z: -2 ray_tracing: true + - name: Blender + type: blender_export + run_by_default: false + options: + outputs: + - type: render + render_options: + transparent_background: true + auto_crop: true + # width: 640 + # height: 480 + - name: 'gerbers' comment: "Gerbers for the Gerber god" type: gerber