diff --git a/examples/align_fastener_holes.py b/examples/align_fastener_holes.py new file mode 100644 index 0000000..ab49c5a --- /dev/null +++ b/examples/align_fastener_holes.py @@ -0,0 +1,93 @@ +""" + +Align Fastener Holes Example + +name: align_fastener_holes.py +by: Gumyr +date: December 11th 2021 + +desc: Example of using the pushFastenerLocations() method to align cq_warehouse.fastener + holes between to plates in an assembly. + +license: + + Copyright 2021 Gumyr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +""" +import cadquery as cq +from cq_warehouse.fastener import SocketHeadCapScrew + +# Create the screws that will fasten the plates together +cap_screw = SocketHeadCapScrew( + size="M2-0.4", length=6, fastener_type="iso4762", simple=False +) + +# Two assemblies are required - the top will contain the screws +bracket_assembly = cq.Assembly(None, name="top_plate_assembly") +square_tube_assembly = cq.Assembly(None, name="base_plate_assembly") + +# --- Angle Bracket --- + +# Create an angle bracket and add clearance holes for the screws +angle_bracket = ( + cq.Workplane("YZ") + .moveTo(-9, 1) + .hLine(10) + .vLine(-10) + .offset2D(1) + .extrude(10, both=True) + .faces(">Z") + .workplane() + .pushPoints([(5, -5), (-5, -5)]) + .clearanceHole(fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly) + .faces(">Y") + .workplane() + .pushPoints([(0, -7)]) + .clearanceHole(fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly) +) +# Add the top plate to the top assembly so it can be placed with the screws +bracket_assembly.add(angle_bracket, name="angle_bracket") +# Add the top plate and screws to the base assembly +square_tube_assembly.add( + bracket_assembly, + name="top_plate_assembly", + loc=cq.Location(cq.Vector(20, 10, 10)), +) + +# --- Square Tube --- + +# Create the square tube +square_tube = ( + cq.Workplane("YZ").rect(18, 18).rect(14, 14).offset2D(1).extrude(30, both=True) +) +# Complete the square tube assembly by adding the square tube +square_tube_assembly.add(square_tube, name="square_tube") +# Add tap holes to the square tube that align with the angle bracket +square_tube = square_tube.pushFastenerLocations( + cap_screw, square_tube_assembly +).tapHole(fastener=cap_screw, counterSunk=False, depth=10) + + +# Where are the cap screw holes in the square tube? +for loc in square_tube_assembly.fastenerLocations(cap_screw): + print(loc) + +# How many fasteners are used in the square_tube_assembly and all sub-assemblies +print(square_tube_assembly.fastenerQuantities()) + +if "show_object" in locals(): + show_object(angle_bracket, name="angle_bracket") + show_object(square_tube, name="square_tube") + show_object(square_tube_assembly, name="square_tube_assembly") diff --git a/setup.cfg b/setup.cfg index 91a2e67..9a81a8a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = cq_warehouse -version = 0.4.1 +version = 0.4.2 author = Gumyr author_email = gumyr9@gmail.com description = A cadquery parametric part collection diff --git a/src/cq_warehouse.egg-info/PKG-INFO b/src/cq_warehouse.egg-info/PKG-INFO index 9045f7c..307e646 100644 --- a/src/cq_warehouse.egg-info/PKG-INFO +++ b/src/cq_warehouse.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cq-warehouse -Version: 0.4.1 +Version: 0.4.2 Summary: A cadquery parametric part collection Home-page: https://github.com/gumyr/cq_warehouse Author: Gumyr diff --git a/src/cq_warehouse/fastener.py b/src/cq_warehouse/fastener.py index a089023..4189918 100644 --- a/src/cq_warehouse/fastener.py +++ b/src/cq_warehouse/fastener.py @@ -30,6 +30,7 @@ limitations under the License. """ +from functools import reduce from abc import ABC, abstractmethod from typing import Literal, Tuple, Optional, List, TypeVar, Union from math import sin, cos, tan, radians, pi, degrees, sqrt @@ -1968,10 +1969,8 @@ def _fastenerHole( ), ) washer_thicknesses += washer.washer_thickness - if hasattr(baseAssembly, "metadata"): - baseAssembly.metadata[baseAssembly.children[-1].name] = washer - else: - baseAssembly.metadata = {baseAssembly.children[-1].name: washer} + # Create a metadata entry associating the auto-generated name & fastener + baseAssembly.metadata[baseAssembly.children[-1].name] = washer baseAssembly.add( fastener.cq_object, @@ -1981,10 +1980,8 @@ def _fastenerHole( * (head_offset - fastener.length_offset() - washer_thicknesses) ), ) - if hasattr(baseAssembly, "metadata"): - baseAssembly.metadata[baseAssembly.children[-1].name] = fastener - else: - baseAssembly.metadata = {baseAssembly.children[-1].name: fastener} + # Create a metadata entry associating the auto-generated name & fastener + baseAssembly.metadata[baseAssembly.children[-1].name] = fastener # Make holes in the stack solid object part = self.cutEach(lambda loc: fastener_hole.moved(loc), True, False) @@ -2096,19 +2093,23 @@ def _threadedHole( cq.Workplane.threadedHole = _threadedHole -def _fastener_quantities(self, bom: bool = True) -> dict: +def _fastener_quantities(self, bom: bool = True, deep: bool = True) -> dict: """Generate a bill of materials of the fasteners in an assembly augmented by the hole methods bom: returns fastener.info if True else fastener """ - if self.metadata is None: - return None + assembly_list = [] + if deep: + for _name, sub_assembly in self.traverse(): + assembly_list.append(sub_assembly) + else: + assembly_list.append(self) + + fasteners = [] + for sub_assembly in assembly_list: + for value in sub_assembly.metadata.values(): + if isinstance(value, (Screw, Nut, Washer)): + fasteners.append(value) - # Extract a list of only the fasteners from the metadata - fasteners = [ - value - for value in self.metadata.values() - if isinstance(value, (Screw, Nut, Washer)) - ] unique_fasteners = set(fasteners) if bom: quantities = {f.info: fasteners.count(f) for f in unique_fasteners} @@ -2117,4 +2118,68 @@ def _fastener_quantities(self, bom: bool = True) -> dict: return quantities -cq.Assembly.fastener_quantities = _fastener_quantities +cq.Assembly.fastenerQuantities = _fastener_quantities + + +def _location_str(self): + """A __str__ method to the Location class""" + loc_tuple = self.toTuple() + return f"({str(loc_tuple[0])}, {str(loc_tuple[1])})" + + +cq.Location.__str__ = _location_str + + +def _fastener_locations(self, fastener: Union[Nut, Screw]) -> list[cq.Location]: + """Generate a list of cadquery Locations for the given fastener relative to the Assembly""" + + name_to_fastener = {} + base_assembly_structure = {} + # Extract a list of only the fasteners from the metadata + for (name, a) in self.traverse(): + base_assembly_structure[name] = a + if a.metadata is None: + continue + + for key, value in a.metadata.items(): + if value == fastener: + name_to_fastener[key] = value + + fastener_path_locations = {} + base_assembly_path = self._flatten() + for assembly_name, _assembly_pointer in base_assembly_path.items(): + for fastener_name in name_to_fastener.keys(): + if fastener_name in assembly_name: + parents = assembly_name.split("/") + fastener_path_locations[fastener_name] = [ + base_assembly_structure[name].loc for name in parents + ] + + fastener_locations = [ + reduce(lambda l1, l2: l1 * l2, locs) + for locs in fastener_path_locations.values() + ] + + return fastener_locations + + +cq.Assembly.fastenerLocations = _fastener_locations + + +def _push_fastener_locations( + self: T, + fastener: Union[Nut, Screw], + baseAssembly: cq.Assembly, +): + """Push the Location(s) of the given fastener relative to the given Assembly onto the stack""" + + # The locations need to be pushed as global not local object locations + ns = self.__class__() + ns.plane = cq.Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)) + ns.parent = self + ns.objects = baseAssembly.fastenerLocations(fastener) + ns.ctx = self.ctx + return ns + + +cq.Workplane.pushFastenerLocations = _push_fastener_locations diff --git a/src/cq_warehouse/socket_head_cap_parameters.csv b/src/cq_warehouse/socket_head_cap_parameters.csv index 439c712..b5d074d 100644 --- a/src/cq_warehouse/socket_head_cap_parameters.csv +++ b/src/cq_warehouse/socket_head_cap_parameters.csv @@ -1,5 +1,5 @@ Size,iso4762:dk ,iso4762:k ,iso4762:s ,iso4762:t ,iso4762:short,iso4762:long,asme_b18.3:dk ,asme_b18.3:k ,asme_b18.3:s ,asme_b18.3:t ,asme_b18.3:short,asme_b18.3:long -M1.6-0.35 ,3.14,1.6,1.5,0.7,2.5,16,,,,,, +M1.6-0.35,3.14,1.6,1.5,0.7,2.5,16,,,,,, M2-0.4,3.98,2,1.5,1,3,20,,,,,, M2.5-0.45,4.68,2.5,2,1.1,4,25,,,,,, M3-0.5,5.68,3,2.5,1.3,5,30,,,,,, diff --git a/tests/fastener_tests.py b/tests/fastener_tests.py index 1e0aae6..75bbc5f 100644 --- a/tests/fastener_tests.py +++ b/tests/fastener_tests.py @@ -341,8 +341,8 @@ def test_clearance_hole(self): ) self.assertLess(box.Volume(), 1000) self.assertEqual(len(pillow_block.children), 1) - self.assertEqual(pillow_block.fastener_quantities(bom=False)[screw], 1) - self.assertEqual(len(pillow_block.fastener_quantities(bom=True)), 1) + self.assertEqual(pillow_block.fastenerQuantities(bom=False)[screw], 1) + self.assertEqual(len(pillow_block.fastenerQuantities(bom=True)), 1) def test_invalid_clearance_hole(self): for fastener_class in Screw.__subclasses__() + Nut.__subclasses__(): @@ -436,6 +436,70 @@ def test_threaded_hole(self): ) self.assertLess(box.Volume(), 8000) self.assertEqual(len(pillow_block.children), 3) + self.assertEqual(len(pillow_block.fastenerLocations(screw)), 1) + + def test_push_fastener_locations(self): + # Create the screws that will fasten the plates together + cap_screw = SocketHeadCapScrew( + size="M2-0.4", length=6, fastener_type="iso4762", simple=False + ) + + # Two assemblies are required - the top will contain the screws + bracket_assembly = cq.Assembly(None, name="top_plate_assembly") + square_tube_assembly = cq.Assembly(None, name="base_plate_assembly") + + # Create an angle bracket and add clearance holes for the screws + angle_bracket = ( + cq.Workplane("YZ") + .moveTo(-9, 1) + .hLine(10) + .vLine(-10) + .offset2D(1) + .extrude(10, both=True) + .faces(">Z") + .workplane() + .pushPoints([(5, -5), (-5, -5)]) + .clearanceHole( + fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly + ) + .faces(">Y") + .workplane() + .pushPoints([(0, -7)]) + .clearanceHole( + fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly + ) + ) + # Add the top plate to the top assembly so it can be placed with the screws + bracket_assembly.add(angle_bracket, name="angle_bracket") + # Add the top plate and screws to the base assembly + square_tube_assembly.add( + bracket_assembly, + name="top_plate_assembly", + loc=cq.Location(cq.Vector(20, 10, 10)), + ) + + # Create the square tube + square_tube = ( + cq.Workplane("YZ") + .rect(18, 18) + .rect(14, 14) + .offset2D(1) + .extrude(30, both=True) + ) + original_tube_volume = square_tube.val().Volume() + # Complete the square tube assembly by adding the square tube + square_tube_assembly.add(square_tube, name="square_tube") + # Add tap holes to the square tube that align with the angle bracket + square_tube = square_tube.pushFastenerLocations( + cap_screw, square_tube_assembly + ).tapHole(fastener=cap_screw, counterSunk=False, depth=10) + self.assertLess(square_tube.val().Volume(), original_tube_volume) + + # Where are the cap screw holes in the square tube? + fastener_positions = [(25.0, 5.0, 12.0), (15.0, 5.0, 12.0), (20.0, 12.0, 5.0)] + for i, loc in enumerate(square_tube_assembly.fastenerLocations(cap_screw)): + self.assertTupleAlmostEquals(loc.toTuple()[0], fastener_positions[i], 7) + self.assertTrue(str(fastener_positions[i]) in str(loc)) if __name__ == "__main__":