diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d665973..83a3de6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,8 +47,9 @@ jobs: ./generate_static.py - name: Generate example docs - run: | - cadorchestrator generate '["NUC10i5FNH", "Raspberry_Pi_4B", "Raspberry_Pi_4B"]' + uses: coactions/setup-xvfb@v1 + with: + run: cadorchestrator --headless generate "[\"NUC10i5FNH\", \"Raspberry_Pi_4B\", \"Raspberry_Pi_4B\"]" - name: Upload artifact uses: actions/upload-pages-artifact@v1 diff --git a/mechanical/assembly_renderer.py b/mechanical/assembly_renderer.py index 81fe758..0c93fab 100644 --- a/mechanical/assembly_renderer.py +++ b/mechanical/assembly_renderer.py @@ -23,7 +23,7 @@ from nimble_build_system.cad.shelf import create_shelf_for assembly_definition_file = "../build/assembly-def.yaml" - +render_destination = os.path.join(os.getcwd(), "renders") class PartDefinition: """ @@ -70,15 +70,21 @@ def generate(self) -> cq.Assembly: """ Generate the assembly. """ + # Make sure that the render destination exists + os.makedirs(render_destination, exist_ok=True) + assembly = cq.Assembly() for part in self._parts: if part.device: # This is a shelf and we load it directly rather than from an STEP. shelf_obj = create_shelf_for(part.device) - cq_part = shelf_obj.generate_assembly_model() - # generate all render pngs for this shelf - # commented out as this doesnt work yet - # self_obj.generate_renders() + + # Create the shelf that will go in the assembly + cq_part = shelf_obj.generate_assembly_model( + shelf_obj.renders["assembled"]["render_options"]) + + # Generate all render pngs for this shelf + shelf_obj.generate_renders(base_path=render_destination) else: cq_part = cq.importers.importStep(part.step_file) for tag in part.tags: diff --git a/nimble_build_system/cad/renderer.py b/nimble_build_system/cad/renderer.py new file mode 100644 index 0000000..a69ebfe --- /dev/null +++ b/nimble_build_system/cad/renderer.py @@ -0,0 +1,33 @@ +#pylint: disable=too-few-public-methods +#pylint: disable=unused-import + +import cadquery as cq +import cadquery_png_plugin.plugin # This activates the PNG plugin for CadQuery +from cq_annotate.callouts import add_assembly_lines + +def generate_render(model=None, image_format="png", file_path=None, render_options=None): + """ + Generates a render of an assembly. + + parameters: + model (cadquery.Assembly): The assembly to render + image_format (str): The format of the image to render (png, svg, gltf, etc) + file_path (str): The path to save the rendered image to + render_options (dict): A dictionary of options to pass to the render function for + things like which view to render,etc + + returns: + None + """ + + # Check to see if we are dealing with a single part or an assembly + if isinstance(model, cq.Assembly): + # Handle assembly annotation + if render_options["annotate"]: + add_assembly_lines(model) + + # Handle the varioius image formats separately + if image_format == "png": + model.exportPNG(options=render_options, file_path=file_path) + else: + print("Unknown image format") diff --git a/nimble_build_system/cad/shelf.py b/nimble_build_system/cad/shelf.py index 4c54125..7aaad3b 100644 --- a/nimble_build_system/cad/shelf.py +++ b/nimble_build_system/cad/shelf.py @@ -15,16 +15,15 @@ import yaml from cadorchestrator.components import AssembledComponent, GeneratedMechanicalComponent import cadquery as cq -import cadquery_png_plugin.plugin # This activates the PNG plugin for CadQuery import cadscript from cq_warehouse.fastener import ButtonHeadScrew, CounterSunkScrew, PanHeadScrew from cq_annotate.views import explode_assembly -from cq_annotate.callouts import add_assembly_lines from nimble_build_system.cad import RackParameters from nimble_build_system.cad.device_placeholder import generate_placeholder from nimble_build_system.cad.shelf_builder import ShelfBuilder, ziptie_shelf from nimble_build_system.cad.fasteners import Screw, Ziptie +from nimble_build_system.cad.renderer import generate_render from nimble_build_system.orchestration.device import Device from nimble_build_system.orchestration.paths import REL_MECH_DIR @@ -116,7 +115,7 @@ class Shelf(): _hole_locations = None # List of hole locations for the device _fasteners = [] # List of screw positions for the device _unit_width = 6 # 6 or 10 inch rack - _render_options = None + _renders = None # Renders that are available for each shelf type # Hole location parameters _screw_dist_x = None @@ -186,15 +185,22 @@ def __init__(self, axis="-X", length=300), ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "front-top-right", - "standard_view": "front-top-right", - "annotated_view": "back-bottom-right", - "add_device_offset": False, - "add_fastener_length": False, - "zoom": 1.0, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "front-top-right", + "zoom": 1.0, + "add_device_offset": False, + "add_fastener_length": False, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "back-bottom-right", + "add_device_offset": False, + "add_fastener_length": False, + "zoom": 1.0, + "annotate": True, + "explode": True}}} # Render options for the shelf @property @@ -357,19 +363,11 @@ def hole_locations(self, value): @property - def render_options(self): - """ - Return the options for rendering the shelf. - """ - return self._render_options - - - @render_options.setter - def render_options(self, value): + def renders(self): """ - Set the options for rendering the shelf. + All of the renders that are available for a shelf and their render options. """ - self._render_options = value + return self._renders def generate_device_model(self): @@ -403,7 +401,7 @@ def generate_shelf_model(self): return self._shelf_model - def generate_assembly_model(self, explode=False): + def generate_assembly_model(self, render_options=None): """ Generates an CAD model of the shelf assembly showing assembly step between a device and a shelf. This can be optionally be exploded. @@ -418,185 +416,183 @@ def generate_assembly_model(self, explode=False): # pylint: disable=too-many-statements # pylint: disable=too-many-function-args - # If the shelf assembly has already been generated, do not generate it again - if self._shelf_assembly_model is None: - # Get and orient the device model properly in relation to the shelf - device = self.generate_device_model() - if self._device_depth_axis == "X": - device = device.rotateAboutCenter((0, 0, 1), 90) - elif self._device_depth_axis == "-X": - device = device.rotateAboutCenter((0, 0, 1), -90) - elif self._device_depth_axis == "Y": - device = device.rotateAboutCenter((0, 0, 1), 0) # No rotation needed - elif self._device_depth_axis == "-Y": - device = device.rotateAboutCenter((0, 0, 1), -180) - elif self._device_depth_axis == "Z": - device = device.rotateAboutCenter((0, 1, 0), 90) - elif self._device_depth_axis == "-Z": - device = device.rotateAboutCenter((0, 1, 0), -90) - - # Move the device to the correct position on the shelf - device = device.translate((self._device_offset[0], - self._device_offset[1], - self._device_offset[2])) - - # Create the assembly holding all the parts that go into the shelf unit - assy = cq.Assembly() - assy.add(device, name="device", - color=cq.Color(0.996, 0.867, 0.0, 1.0), - metadata={ - "explode_translation": cq.Location(self._device_explode_translation) - }) - assy.add(self.generate_shelf_model().cq(), - name="shelf", - color=cq.Color(0.565, 0.698, 0.278, 1.0)) - - # Add the fasteners to the assembly - for i, fastener in enumerate(self._fasteners): - # Handle the different fastener types - if fastener.fastener_type == "ziptie": - # Create the ziptie spine - cur_fastener = cq.Workplane().box(fastener.width, - fastener.length, - fastener.thickness) - - # Create the ziptie head - cur_fastener = (cur_fastener.faces(">Z") - .workplane(invert=True) - .move(0.0, fastener.length / 2.0) - .rect(fastener.width + 2.0, - fastener.width + 2.0) - .extrude(fastener.thickness + 3.0)) - - # Chamfer the insertion end of the ziptie - cur_fastener = (cur_fastener.faces(">Y") - .edges(">X and |Z") - .chamfer(length=fastener.width / 4.0, - length2=fastener.width * 2.0)) - cur_fastener = (cur_fastener.faces(">Y") - .edges("Z") - .workplane(invert=True) - .move(0.0, -(fastener.length / 2.0)) - .rect(fastener.width, fastener.thickness) - .cutThruAll()) - else: - if fastener.fastener_type == "iso10642": - # Create the counter-sunk screw model - cur_fastener = cq.Workplane(CounterSunkScrew(size=fastener.size, - fastener_type=fastener.fastener_type, - length=fastener.length, - simple=True).cq_object) - elif fastener.fastener_type == "asme_b_18.6.3": - # Create the cheesehead screw model - cur_fastener = cq.Workplane(PanHeadScrew(size=fastener.size, - fastener_type=fastener.fastener_type, - length=fastener.length, - simple=True).cq_object) - else: - # Create a button head screw model - cur_fastener = cq.Workplane(ButtonHeadScrew(size=fastener.size, - fastener_type=fastener.fastener_type, - length=fastener.length, - simple=True).cq_object) - - - # Allows the proper face to be selected for the extension lines - face_selector = "Z") + .workplane(invert=True) + .move(0.0, fastener.length / 2.0) + .rect(fastener.width + 2.0, + fastener.width + 2.0) + .extrude(fastener.thickness + 3.0)) + + # Chamfer the insertion end of the ziptie + cur_fastener = (cur_fastener.faces(">Y") + .edges(">X and |Z") + .chamfer(length=fastener.width / 4.0, + length2=fastener.width * 2.0)) + cur_fastener = (cur_fastener.faces(">Y") + .edges("Z") + .workplane(invert=True) + .move(0.0, -(fastener.length / 2.0)) + .rect(fastener.width, fastener.thickness) + .cutThruAll()) + else: + if fastener.fastener_type == "iso10642": + # Create the counter-sunk screw model + cur_fastener = cq.Workplane(CounterSunkScrew(size=fastener.size, + fastener_type=fastener.fastener_type, + length=fastener.length, + simple=True).cq_object) + elif fastener.fastener_type == "asme_b_18.6.3": + # Create the cheesehead screw model + cur_fastener = cq.Workplane(PanHeadScrew(size=fastener.size, + fastener_type=fastener.fastener_type, + length=fastener.length, + simple=True).cq_object) else: + # Create a button head screw model + cur_fastener = cq.Workplane(ButtonHeadScrew(size=fastener.size, + fastener_type=fastener.fastener_type, + length=fastener.length, + simple=True).cq_object) + + + # Allows the proper face to be selected for the extension lines + face_selector = " cadscript.Body: @@ -837,15 +827,22 @@ def __init__(self, axis="-Z", length=8), ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "front-top-right", - "standard_view": "front-top-right", - "annotated_view": "front-bottom-right", - "add_device_offset": True, - "add_fastener_length": True, - "zoom": 1.15, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "front-top-right", + "zoom": 1.15, + "add_device_offset": False, + "add_fastener_length": False, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "front-bottom-right", + "add_device_offset": True, + "add_fastener_length": True, + "zoom": 1.15, + "annotate": True, + "explode": True}}} # Render options for the shelf def generate_shelf_model(self) -> cadscript.Body: @@ -933,15 +930,22 @@ def __init__(self, axis="-Y", length=4), ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "front-top-right", - "standard_view": "front-top-right", - "annotated_view": "back-top-right", - "add_device_offset": False, - "add_fastener_length": True, - "zoom": 1.0, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "front-top-right", + "zoom": 1.0, + "add_device_offset": False, + "add_fastener_length": False, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "back-top-right", + "add_device_offset": False, + "add_fastener_length": True, + "zoom": 1.0, + "annotate": True, + "explode": True}}} # Render options for the shelf def generate_shelf_model(self) -> cadscript.Body: @@ -1078,15 +1082,22 @@ def __init__(self, axis="X", length=6), ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "front-top-right", - "standard_view": "front-top-right", - "annotated_view": "back-bottom-right", - "add_device_offset": False, - "add_fastener_length": True, - "zoom": 1.15, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "front-top-right", + "zoom": 1.15, + "add_device_offset": False, + "add_fastener_length": False, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "back-bottom-right", + "add_device_offset": False, + "add_fastener_length": True, + "zoom": 1.15, + "annotate": True, + "explode": True}}} # Render options for the shelf def generate_shelf_model(self) -> cadscript.Body: @@ -1189,15 +1200,22 @@ def __init__(self, axis="X", length=6), ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "front-top-right", - "standard_view": "front-top-right", - "annotated_view": "back-bottom-right", - "add_device_offset": False, - "add_fastener_length": False, - "zoom": 1.15, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "front-top-right", + "zoom": 1.15, + "add_device_offset": False, + "add_fastener_length": False, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "back-bottom-right", + "add_device_offset": False, + "add_fastener_length": True, + "zoom": 1.15, + "annotate": True, + "explode": True}}} # Renders for the shelf def generate_shelf_model(self) -> cadscript.Body: @@ -1309,15 +1327,22 @@ def __init__(self, axis="Z", length=6) ] - self.render_options = { - "color_theme": "default", # can also use black_and_white - "view": "back-top-right", - "standard_view": "back-top-right", - "annotated_view": "back-top-right", - "add_device_offset": False, - "add_fastener_length": True, - "zoom": 1.25, - } + self._renders = {"assembled": + {"render_options": {"color_theme": "default", + "view": "back-top-right", + "add_device_offset": False, + "add_fastener_length": False, + "zoom": 1.25, + "annotate": False, + "explode": False}}, + "annotated": + {"render_options": {"color_theme": "default", + "view": "back-top-right", + "add_device_offset": False, + "add_fastener_length": True, + "zoom": 1.25, + "annotate": True, + "explode": True}}} # Renders for the shelf def generate_shelf_model(self): diff --git a/tests/test_cad.py b/tests/test_cad.py index 29f2183..46d5bb9 100644 --- a/tests/test_cad.py +++ b/tests/test_cad.py @@ -154,7 +154,7 @@ def test_shelf_assembly_generation(): assert rpi_shelf != None # Test the generated CAD assembly - assy = rpi_shelf.generate_assembly_model() + assy = rpi_shelf.generate_assembly_model(rpi_shelf.renders["assembled"]["render_options"]) # Make sure the assembly has the number of children we expect assert len(assy.children) == 6 @@ -165,7 +165,8 @@ def test_shelf_assembly_generation(): assert intersection_part.Volume() == pytest.approx(0.0, 0.001) # Test exploded assembly - exploded_assy = rpi_shelf.generate_assembly_model(explode=True) + exploded_assy = rpi_shelf.generate_assembly_model( + rpi_shelf.renders["annotated"]["render_options"]) # Make sure the assembly has the number of children we expect assert len(assy.children) == 6 diff --git a/tests/test_rendering.py b/tests/test_rendering.py index af6e80a..c17005f 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -17,51 +17,22 @@ ] -def test_assembly_png_rendering(): +def test_png_rendering(): """ - Tests whether or not a PNG image can be output for an entire assembly. + Tests whether or not a PNG image can be output for each render of a shelf assembly. """ - - # Load the needed information to generate a Shelf object - config = NimbleConfiguration(test_config) - - # Check all of the shelf assemblies - for cur_shelf in config.shelves: - # Test the generated CAD assembly - shelf_assy = cur_shelf.generate_assembly_model() - - # Set up a temporary path to export the image to - temp_dir = tempfile.gettempdir() - temp_path = os.path.join(temp_dir, "assembly_render_test_" + cur_shelf._device.name + ".png") - - # Do a sample render of the shelf assembly - cur_shelf.get_render(shelf_assy, - file_path=temp_path) - - assert os.path.isfile(temp_path) - - -def test_annotated_assembly_png_rendering(): - """ - Tests whether or not a PNG image can be output for an entire assembly with - annotations (i.e. assembly lines). - """ - - # Load the needed information to generate a Shelf object + # Load the needed information to generate a Shelf object config = NimbleConfiguration(test_config) # Check all of the shelf assemblies for cur_shelf in config.shelves: - # Test the generated CAD assembly - shelf_assy = cur_shelf.generate_assembly_model(explode=True) # Set up a temporary path to export the image to temp_dir = tempfile.gettempdir() - temp_path = os.path.join(temp_dir, "assembly_render_test_exploded_" + cur_shelf._device.name + ".png") # Do a sample render of the shelf assembly - cur_shelf.get_render(shelf_assy, - file_path=temp_path, - annotate=True) + cur_shelf.generate_renders(temp_dir) - assert os.path.isfile(temp_path) + # Check to make sure that all of the appropriate files were created + for render_file in cur_shelf.list_render_files(): + assert os.path.isfile(os.path.join(temp_dir, render_file))