diff --git a/freecad/gridfinity_workbench/baseplate_feature_construction.py b/freecad/gridfinity_workbench/baseplate_feature_construction.py index 07c1311..895c3be 100644 --- a/freecad/gridfinity_workbench/baseplate_feature_construction.py +++ b/freecad/gridfinity_workbench/baseplate_feature_construction.py @@ -9,142 +9,19 @@ import Part from . import const, utils +from . import magnet_hole as magnet_hole_module from .utils import GridfinityLayout -def _magnet_hole_hex( - obj: fc.DocumentObject, - x_hole_pos: float, - y_hole_pos: float, -) -> Part.Shape: - # Ratio of 2/sqrt(3) converts from inscribed circle radius to circumscribed circle radius - radius = obj.MagnetHoleDiameter / math.sqrt(3) - - n_sides = 6 - rot = fc.Rotation(fc.Vector(0, 0, 1), 0) - - p = fc.ActiveDocument.addObject("Part::RegularPolygon") - p.Polygon = n_sides - p.Circumradius = radius - p.Placement = fc.Placement(fc.Vector(-x_hole_pos, -y_hole_pos), rot) - p.recompute() - f = Part.Face(Part.Wire(p.Shape.Edges)) - c1 = f.extrude(fc.Vector(0, 0, -obj.MagnetHoleDepth)) - fc.ActiveDocument.removeObject(p.Name) - - p = fc.ActiveDocument.addObject("Part::RegularPolygon") - p.Polygon = n_sides - p.Circumradius = radius - p.Placement = fc.Placement( - fc.Vector(x_hole_pos, -y_hole_pos), - rot, - ) - p.recompute() - f = Part.Face(Part.Wire(p.Shape.Edges)) - c2 = f.extrude(fc.Vector(0, 0, -obj.MagnetHoleDepth)) - fc.ActiveDocument.removeObject(p.Name) - - p = fc.ActiveDocument.addObject("Part::RegularPolygon") - p.Polygon = n_sides - p.Circumradius = radius - p.Placement = fc.Placement(fc.Vector(-x_hole_pos, y_hole_pos), rot) - p.recompute() - f = Part.Face(Part.Wire(p.Shape.Edges)) - c3 = f.extrude(fc.Vector(0, 0, -obj.MagnetHoleDepth)) - fc.ActiveDocument.removeObject(p.Name) - - p = fc.ActiveDocument.addObject("Part::RegularPolygon") - p.Polygon = n_sides - p.Circumradius = radius - p.Placement = fc.Placement(fc.Vector(x_hole_pos, y_hole_pos), rot) - p.recompute() - f = Part.Face(Part.Wire(p.Shape.Edges)) - c4 = f.extrude(fc.Vector(0, 0, -obj.MagnetHoleDepth)) - fc.ActiveDocument.removeObject(p.Name) - - return c1.multiFuse([c2, c3, c4]) - - -def _magnet_hole_round( - obj: fc.DocumentObject, - x_hole_pos: float, - y_hole_pos: float, -) -> Part.Shape: - c = [ - Part.makeCylinder( - obj.MagnetHoleDiameter / 2, - obj.MagnetHoleDepth, - pos, - fc.Vector(0, 0, -1), - ) - for pos in utils.corners(x_hole_pos, y_hole_pos) - ] - - # Chamfer - ct = [ - Part.makeCircle( - obj.MagnetHoleDiameter / 2 + obj.MagnetChamfer, - pos, - fc.Vector(0, 0, 1), - ) - for pos in utils.corners(x_hole_pos, y_hole_pos) - ] - cb = [ - Part.makeCircle( - obj.MagnetHoleDiameter / 2, - pos, - fc.Vector(0, 0, 1), - ) - for pos in utils.corners(x_hole_pos, y_hole_pos, -obj.MagnetChamfer) - ] - - ch = [Part.makeLoft([t, b], solid=True) for t, b in zip(ct, cb)] - - return utils.multi_fuse(c + ch) - - def magnet_holes_properties(obj: fc.DocumentObject) -> None: - """Make baseplate magnet holes. - - Args: - obj (FreeCAD.DocumentObject): Document object. - - """ - ## Gridfinity Non Standard Parameters - - obj.addProperty( - "App::PropertyEnumeration", - "MagnetHolesShape", - "NonStandard", - ( - "Shape of magnet holes, change to suit your printers capabilities which" - "might require testing." - "
Round press fit by default, increase to 6.5 mm if using glue" - "

Hex is alternative press fit style." - "

default = 6.2 mm" - ), + """Make baseplate magnet holes.""" + magnet_hole_module.add_properties( + obj, + remove_channel=False, + chamfer=True, + magnet_holes_default=True, ) - - obj.MagnetHolesShape = const.HOLE_SHAPES - - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDiameter", - "NonStandard", - ( - "Diameter of Magnet Holes" - "
Round press fit by default, increase to 6.5 mm if using glue" - "

Hex is alternative press fit style, inscribed diameter

" - "

default = 6.2 mm" - ), - ).MagnetHoleDiameter = const.MAGNET_HOLE_DIAMETER - - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDepth", - "NonStandard", - "Depth of Magnet Holes

default = 2.4 mm", - ).MagnetHoleDepth = const.MAGNET_HOLE_DEPTH + obj.setEditorMode("MagnetHoles", ("ReadOnly", "Hidden")) obj.addProperty( "App::PropertyLength", @@ -169,22 +46,6 @@ def magnet_holes_properties(obj: fc.DocumentObject) -> None: "

default = 3 mm", ).MagnetBaseHole = const.MAGNET_BASE_HOLE - obj.addProperty( - "App::PropertyLength", - "MagnetChamfer", - "NonStandard", - "Chamfer at top of magnet hole

default = 0.4 mm", - ).MagnetChamfer = const.MAGNET_CHAMFER - - ## Gridfinity Expert Only Parameters - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDistanceFromEdge", - "zzExpertOnly", - "Distance of the magnet holes from bin edge

default = 8.0 mm", - read_only=True, - ).MagnetHoleDistanceFromEdge = const.MAGNET_HOLE_DISTANCE_FROM_EDGE - ## Gridfinity Hidden Properties obj.addProperty( "App::PropertyLength", @@ -194,14 +55,6 @@ def magnet_holes_properties(obj: fc.DocumentObject) -> None: hidden=True, ).BaseThickness = const.BASE_THICKNESS - obj.addProperty( - "App::PropertyBool", - "MagnetHoles", - "ShouldBeHidden", - "MagnetHoles", - hidden=True, - ).MagnetHoles = const.MAGNET_HOLES - def make_magnet_holes(obj: fc.DocumentObject, layout: GridfinityLayout) -> Part.Shape: """Create magentholes for a baseplate.""" @@ -209,38 +62,25 @@ def make_magnet_holes(obj: fc.DocumentObject, layout: GridfinityLayout) -> Part. y_hole_pos = obj.yGridSize / 2 - obj.MagnetHoleDistanceFromEdge # Magnet holes - if obj.MagnetHolesShape == "Hex": - hm1 = _magnet_hole_hex(obj, x_hole_pos, y_hole_pos) - elif obj.MagnetHolesShape == "Round": - hm1 = _magnet_hole_round(obj, x_hole_pos, y_hole_pos) - else: - raise ValueError(f"Unexpected hole shape: {obj.MagnetHolesShape}") - - # Screw holes - ca = [ - Part.makeCylinder( - obj.MagnetBaseHole / 2, - obj.MagnetHoleDepth + obj.BaseThickness, - pos, - fc.Vector(0, 0, -1), - ) - for pos in utils.corners(x_hole_pos, -y_hole_pos) - ] - - hm1 = hm1.multiFuse(ca) - hm1.translate(fc.Vector(obj.xGridSize / 2, obj.yGridSize / 2)) - - hm2 = utils.copy_in_layout(hm1, layout, obj.xGridSize, obj.yGridSize) - return hm2.translate(fc.Vector(-obj.xLocationOffset, -obj.yLocationOffset)) + shape = magnet_hole_module.from_obj(obj) + shape = shape.translate(fc.Vector(0, 0, -obj.MagnetHoleDepth)) + screw_hole = Part.makeCylinder( + obj.MagnetBaseHole / 2, + obj.MagnetHoleDepth + obj.BaseThickness, + fc.Vector(0, 0, 0), + fc.Vector(0, 0, -1), + ) + shape = shape.fuse(screw_hole) + shape = utils.copy_and_translate(shape, utils.corners(x_hole_pos, y_hole_pos)) + shape.translate(fc.Vector(obj.xGridSize / 2, obj.yGridSize / 2)) -def screw_bottom_chamfer_properties(obj: fc.DocumentObject) -> None: - """Create Baseplate Connection Holes. + shape = utils.copy_in_layout(shape, layout, obj.xGridSize, obj.yGridSize) + return shape.translate(fc.Vector(-obj.xLocationOffset, -obj.yLocationOffset)) - Args: - obj (FreeCAD.DocumentObject): Document object. - """ +def screw_bottom_chamfer_properties(obj: fc.DocumentObject) -> None: + """Create Baseplate Connection Holes.""" ## Gridfinity Non Standard Parameters obj.addProperty( "App::PropertyLength", @@ -264,28 +104,14 @@ def make_screw_bottom_chamfer(obj: fc.DocumentObject, layout: GridfinityLayout) x_hole_pos = obj.xGridSize / 2 - obj.MagnetHoleDistanceFromEdge y_hole_pos = obj.yGridSize / 2 - obj.MagnetHoleDistanceFromEdge - ct_z = -obj.TotalHeight + obj.BaseProfileHeight - ct = [ - Part.makeCircle( - obj.ScrewHoleDiameter / 2 + obj.MagnetBottomChamfer, - pos, - fc.Vector(0, 0, 1), - ) - for pos in utils.corners(x_hole_pos, y_hole_pos, ct_z) - ] - cb_z = -obj.TotalHeight + obj.MagnetBottomChamfer + obj.BaseProfileHeight - cb = [ - Part.makeCircle( - obj.ScrewHoleDiameter / 2, - pos, - fc.Vector(0, 0, 1), - ) - for pos in utils.corners(x_hole_pos, y_hole_pos, cb_z) - ] - - ch = [Part.makeLoft([t, b], solid=True) for t, b in zip(ct, cb)] - - hm1 = utils.multi_fuse(ch) + ch = Part.makeCone( + obj.ScrewHoleDiameter / 2 + obj.MagnetBottomChamfer, + obj.ScrewHoleDiameter / 2, + obj.MagnetBottomChamfer, + fc.Vector(0, 0, -obj.TotalHeight + obj.BaseProfileHeight), + ) + + hm1 = utils.copy_and_translate(ch, utils.corners(x_hole_pos, y_hole_pos)) hm2 = utils.copy_in_layout(hm1, layout, obj.xGridSize, obj.yGridSize) return hm2.translate( fc.Vector(obj.xGridSize / 2 - obj.xLocationOffset, obj.yGridSize / 2 - obj.yLocationOffset), @@ -293,12 +119,7 @@ def make_screw_bottom_chamfer(obj: fc.DocumentObject, layout: GridfinityLayout) def connection_holes_properties(obj: fc.DocumentObject) -> None: - """Create Baseplate Connection Holes. - - Args: - obj (FreeCAD.DocumentObject): Document object. - - """ + """Create Baseplate Connection Holes.""" ## Gridfinity Non Standard Parameters obj.addProperty( "App::PropertyLength", @@ -358,7 +179,7 @@ def make_connection_holes(obj: fc.DocumentObject, layout: GridfinityLayout) -> P return fuse_total -def _center_cut_wire(obj: fc.DocumentObject) -> Part.Wire: +def _center_cut_face(obj: fc.DocumentObject) -> Part.Face: """Create wire for the baseplate center cut.""" x_inframedis = ( obj.xGridSize / 2 @@ -453,7 +274,7 @@ def _center_cut_wire(obj: fc.DocumentObject) -> Part.Wire: l5 = Part.LineSegment(l4.EndPoint, mec_middle) l6 = Part.LineSegment(l5.EndPoint, l1.StartPoint) - return utils.curve_to_wire([l1, ar1, l2, ar2, l3, ar3, l4, l5, l6]) + return utils.curve_to_face([l1, ar1, l2, ar2, l3, ar3, l4, l5, l6]) def center_cut_properties(obj: fc.DocumentObject) -> None: @@ -468,9 +289,9 @@ def center_cut_properties(obj: fc.DocumentObject) -> None: def make_center_cut(obj: fc.DocumentObject, layout: GridfinityLayout) -> Part.Shape: """Create baseplate center cutout.""" - wire = _center_cut_wire(obj) + face = _center_cut_face(obj) - partial_shape1 = Part.Face(wire).extrude(fc.Vector(0, 0, -obj.TotalHeight)) + partial_shape1 = face.extrude(fc.Vector(0, 0, -obj.TotalHeight)) partial_shape2 = partial_shape1.mirror(fc.Vector(0, 0, 0), fc.Vector(0, 1, 0)) partial_shape3 = partial_shape1.mirror(fc.Vector(0, 0, 0), fc.Vector(1, 0, 0)) partial_shape4 = partial_shape2.mirror(fc.Vector(0, 0, 0), fc.Vector(1, 0, 0)) diff --git a/freecad/gridfinity_workbench/const.py b/freecad/gridfinity_workbench/const.py index 102ae15..0344079 100644 --- a/freecad/gridfinity_workbench/const.py +++ b/freecad/gridfinity_workbench/const.py @@ -12,13 +12,15 @@ MAGNET_HOLE_DIAMETER = 6.2 MAGNET_HOLE_DEPTH = 2.4 +CRUSH_RIB_N = 12 +CRUSH_RIB_WAVINESS = 0.5 SCREW_HOLE_DIAMETER = 3 SCREW_HOLE_DEPTH = 6 MAGNET_HOLE_DISTANCE_FROM_EDGE = 8 -HOLE_SHAPES = ["Round", "Hex"] +HOLE_SHAPES = ["Round", "Crush ribs", "Hex"] ## Bins # General Bin Parameters @@ -97,7 +99,6 @@ MAGNET_EDGE_THICKNESS = 1.2 MAGNET_BASE = 0.4 MAGNET_BASE_HOLE = 3 -MAGNET_CHAMFER = 0.4 # Screw Together Baseplate Specific CONNECTION_HOLE_DIAMETER = 3.2 diff --git a/freecad/gridfinity_workbench/feature_construction.py b/freecad/gridfinity_workbench/feature_construction.py index 007a0a4..f5b1114 100644 --- a/freecad/gridfinity_workbench/feature_construction.py +++ b/freecad/gridfinity_workbench/feature_construction.py @@ -8,6 +8,7 @@ from . import const, utils from . import label_shelf as label_shelf_module +from . import magnet_hole as magnet_hole_module unitmm = fc.Units.Quantity("1 mm") zeromm = fc.Units.Quantity("0 mm") @@ -19,7 +20,7 @@ def label_shelf_properties(obj: fc.DocumentObject, *, label_style_default: str) -> None: - """Create bin compartments with the option for dividers. + """Add label shelf properties to an object. Args: obj (FreeCAD.DocumentObject): Document object. @@ -329,7 +330,7 @@ def make_fillet(rotation: float, translation: fc.Vector) -> Part.Shape: Part.LineSegment(v3, v1), ] - face = Part.Face(utils.curve_to_wire(lines)) + face = utils.curve_to_face(lines) face.rotate(fc.Vector(0, 0, 0), fc.Vector(0, 0, 1), rotation) face.translate(translation) return face.extrude(fc.Vector(0, 0, -obj.TotalHeight)) @@ -549,15 +550,11 @@ def make_compartments(obj: fc.DocumentObject, bin_inside_solid: Part.Shape) -> P if obj.xDividerHeight < divmin and obj.xDividerHeight != 0: obj.xDividerHeight = divmin - fc.Console.PrintWarning( - f"Divider Height must be equal to or greater than: {divmin}\n", - ) + fc.Console.PrintWarning(f"Divider Height must be equal to or greater than: {divmin}\n") if obj.yDividerHeight < divmin and obj.yDividerHeight != 0: obj.yDividerHeight = divmin - fc.Console.PrintWarning( - f"Divider Height must be equal to or greater than: {divmin}\n", - ) + fc.Console.PrintWarning(f"Divider Height must be equal to or greater than: {divmin}\n") if ( obj.xDividerHeight < obj.TotalHeight @@ -566,9 +563,7 @@ def make_compartments(obj: fc.DocumentObject, bin_inside_solid: Part.Shape) -> P and obj.xDividers != 0 ): obj.LabelShelfStyle = "Off" - fc.Console.PrintWarning( - "Label Shelf turned off for less than full height x dividers\n", - ) + fc.Console.PrintWarning("Label Shelf turned off for less than full height x dividers\n") ## Compartment Generation if obj.xDividers == 0 and obj.yDividers == 0: @@ -579,100 +574,6 @@ def make_compartments(obj: fc.DocumentObject, bin_inside_solid: Part.Shape) -> P return func_fuse.translate(fc.Vector(-obj.xLocationOffset, -obj.yLocationOffset)) -def make_bottom_hole_shape(obj: fc.DocumentObject) -> Part.Shape: - """Create bottom hole shape. - - Returns one combined shape containing of the different hole types. - """ - sqbr1_depth = obj.MagnetHoleDepth + obj.SequentialBridgingLayerHeight - sqbr2_depth = obj.MagnetHoleDepth + obj.SequentialBridgingLayerHeight * 2 - - bottom_hole_shape: Part.Shape | None = None - - if obj.MagnetHoles: - if obj.MagnetHolesShape == "Hex": - # Ratio of 2/sqrt(3) converts from inscribed circle radius to circumscribed - # circle radius - radius = obj.MagnetHoleDiameter / math.sqrt(3) - p = fc.ActiveDocument.addObject("Part::RegularPolygon") - p.Polygon = 6 - p.Circumradius = radius - p.recompute() - - p_wire: Part.Wire = p.Shape - magnet_hole_shape = Part.Face(p_wire).extrude(fc.Vector(0, 0, obj.MagnetHoleDepth)) - fc.ActiveDocument.removeObject(p.Name) - else: - magnet_hole_shape = Part.makeCylinder( - obj.MagnetHoleDiameter / 2, - obj.MagnetHoleDepth, - fc.Vector(0, 0, 0), - fc.Vector(0, 0, 1), - ) - - bottom_hole_shape = ( - magnet_hole_shape - if bottom_hole_shape is None - else bottom_hole_shape.fuse(magnet_hole_shape) - ) - - if obj.ScrewHoles: - screw_hole_shape = Part.makeCylinder( - obj.ScrewHoleDiameter / 2, - obj.ScrewHoleDepth, - fc.Vector(0, 0, 0), - fc.Vector(0, 0, 1), - ) - - bottom_hole_shape = ( - screw_hole_shape - if bottom_hole_shape is None - else bottom_hole_shape.fuse(screw_hole_shape) - ) - - if obj.ScrewHoles and obj.MagnetHoles: - b1 = Part.makeBox( - obj.ScrewHoleDiameter, - obj.ScrewHoleDiameter, - sqbr2_depth, - fc.Vector(-obj.ScrewHoleDiameter / 2, -obj.ScrewHoleDiameter / 2), - fc.Vector(0, 0, 1), - ) - arc_pt_off_x = ( - math.sqrt( - ((obj.MagnetHoleDiameter / 2) ** 2) - ((obj.ScrewHoleDiameter / 2) ** 2), - ) - ) * unitmm - arc_pt_off_y = obj.ScrewHoleDiameter / 2 - - va1 = fc.Vector(arc_pt_off_x, arc_pt_off_y) - va2 = fc.Vector(-arc_pt_off_x, arc_pt_off_y) - va3 = fc.Vector(-arc_pt_off_x, -arc_pt_off_y) - va4 = fc.Vector(arc_pt_off_x, -arc_pt_off_y) - var1 = fc.Vector(obj.MagnetHoleDiameter / 2, 0) - var2 = fc.Vector(-obj.MagnetHoleDiameter / 2, 0) - line_1 = Part.LineSegment(va1, va2) - line_2 = Part.LineSegment(va3, va4) - ar1 = Part.Arc(va1, var1, va4) - ar2 = Part.Arc(va2, var2, va3) - s1 = Part.Shape([line_1, ar1, ar2, line_2]) - w1 = Part.Wire(s1.Edges) - sq1_1 = Part.Face(w1) - sq1_1 = sq1_1.extrude(fc.Vector(0, 0, sqbr1_depth)) - holes_interface_shape = sq1_1.fuse(b1) - - bottom_hole_shape = ( - holes_interface_shape - if bottom_hole_shape is None - else bottom_hole_shape.fuse(holes_interface_shape) - ) - - if bottom_hole_shape is None: - raise RuntimeError("No bottom_hole_shape to return") - - return bottom_hole_shape - - def _eco_bin_deviders(obj: fc.DocumentObject, xcomp_w: float, ycomp_w: float) -> Part.Shape: stackingoffset = -obj.LabelShelfStackingOffset if obj.StackingLip else zeromm @@ -1114,12 +1015,7 @@ def make_complex_bin_base( def blank_bin_recessed_top_properties(obj: fc.DocumentObject) -> None: - """Create blank bin recessed top section. - - Args: - obj (FreeCAD.DocumentObject): Document object - - """ + """Create blank bin recessed top section.""" ## Gridfinity Non Standard Parameters obj.addProperty( "App::PropertyLength", @@ -1144,14 +1040,14 @@ def bin_bottom_holes_properties(obj: fc.DocumentObject, *, magnet_holes_default: magnet_holes_default (bool): does the object have magnet holes """ - ## Gridfinity Parameters - obj.addProperty( - "App::PropertyBool", - "MagnetHoles", - "Gridfinity", - "Toggle the magnet holes on or off", - ).MagnetHoles = magnet_holes_default + magnet_hole_module.add_properties( + obj, + remove_channel=True, + chamfer=False, + magnet_holes_default=magnet_holes_default, + ) + ## Gridfinity Parameters obj.addProperty( "App::PropertyBool", "ScrewHoles", @@ -1168,38 +1064,6 @@ def bin_bottom_holes_properties(obj: fc.DocumentObject, *, magnet_holes_default: "used for screw holes bridging with magnet holes also on", ).SequentialBridgingLayerHeight = const.SEQUENTIAL_BRIDGING_LAYER_HEIGHT - obj.addProperty( - "App::PropertyEnumeration", - "MagnetHolesShape", - "GridfinityNonStandard", - ( - "Shape of magnet holes, change to suit your printers capabilities" - "which might require testing." - "
Round press fit by default, increase to 6.5 mm if using glue" - "

Hex is alternative press fit style." - "

default = 6.2 mm" - ), - ).MagnetHolesShape = const.HOLE_SHAPES - - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDiameter", - "GridfinityNonStandard", - ( - "Diameter of Magnet Holes " - "
Round press fit by default, increase to 6.5 mm if using glue" - "

Hex is alternative press fit style, inscribed diameter

" - "

default = 6.2 mm" - ), - ).MagnetHoleDiameter = const.MAGNET_HOLE_DIAMETER - - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDepth", - "GridfinityNonStandard", - "Depth of Magnet Holes

default = 2.4 mm", - ).MagnetHoleDepth = const.MAGNET_HOLE_DEPTH - obj.addProperty( "App::PropertyLength", "ScrewHoleDiameter", @@ -1215,14 +1079,41 @@ def bin_bottom_holes_properties(obj: fc.DocumentObject, *, magnet_holes_default: "Depth of Screw Holes

default = 6.0 mm", ).ScrewHoleDepth = const.SCREW_HOLE_DEPTH - ## Expert Only Parameters - obj.addProperty( - "App::PropertyLength", - "MagnetHoleDistanceFromEdge", - "zzExpertOnly", - "Distance of the magnet holes from bin edge

default = 8.0 mm", - read_only=True, - ).MagnetHoleDistanceFromEdge = const.MAGNET_HOLE_DISTANCE_FROM_EDGE + +def _make_holes_interface(obj: fc.DocumentObject) -> Part.Shape: + sqbr1_depth = obj.MagnetHoleDepth + obj.SequentialBridgingLayerHeight + sqbr2_depth = obj.MagnetHoleDepth + obj.SequentialBridgingLayerHeight * 2 + + b1 = Part.makeBox( + obj.ScrewHoleDiameter, + obj.ScrewHoleDiameter, + sqbr2_depth, + fc.Vector(-obj.ScrewHoleDiameter / 2, -obj.ScrewHoleDiameter / 2), + fc.Vector(0, 0, 1), + ) + arc_pt_off_x = ( + math.sqrt( + ((obj.MagnetHoleDiameter / 2) ** 2) - ((obj.ScrewHoleDiameter / 2) ** 2), + ) + ) * unitmm + arc_pt_off_y = obj.ScrewHoleDiameter / 2 + + va1 = fc.Vector(arc_pt_off_x, arc_pt_off_y) + va2 = fc.Vector(-arc_pt_off_x, arc_pt_off_y) + va3 = fc.Vector(-arc_pt_off_x, -arc_pt_off_y) + va4 = fc.Vector(arc_pt_off_x, -arc_pt_off_y) + var1 = fc.Vector(obj.MagnetHoleDiameter / 2, 0) + var2 = fc.Vector(-obj.MagnetHoleDiameter / 2, 0) + line_1 = Part.LineSegment(va1, va2) + line_2 = Part.LineSegment(va3, va4) + ar1 = Part.Arc(va1, var1, va4) + ar2 = Part.Arc(va2, var2, va3) + s1 = Part.Shape([line_1, ar1, ar2, line_2]) + w1 = Part.Wire(s1.Edges) + sq1_1 = Part.Face(w1) + sq1_1 = sq1_1.extrude(fc.Vector(0, 0, sqbr1_depth)) + + return sq1_1.fuse(b1) def make_bin_bottom_holes( @@ -1230,19 +1121,31 @@ def make_bin_bottom_holes( layout: GridfinityLayout, ) -> Part.Shape: """Make bin bottom holes.""" - bottom_hole_shape = make_bottom_hole_shape(obj) + shapes = [] + if obj.MagnetHoles: + shapes.append(magnet_hole_module.from_obj(obj)) + if obj.ScrewHoles: + shapes.append(Part.makeCylinder(obj.ScrewHoleDiameter / 2, obj.ScrewHoleDepth)) + if obj.ScrewHoles and obj.MagnetHoles: + shapes.append(_make_holes_interface(obj)) + shape = utils.multi_fuse(shapes) + + x_pos = obj.xGridSize / 2 - obj.MagnetHoleDistanceFromEdge + y_pos = obj.yGridSize / 2 - obj.MagnetHoleDistanceFromEdge + shape = utils.copy_and_translate(shape, utils.corners(x_pos, y_pos, -obj.TotalHeight)) - x_hole_pos = obj.xGridSize / 2 - obj.MagnetHoleDistanceFromEdge - y_hole_pos = obj.yGridSize / 2 - obj.MagnetHoleDistanceFromEdge + if obj.MagnetHoles and obj.MagnetRemoveChannel: + remove_channel = magnet_hole_module.remove_channel(obj).translate( + fc.Vector(0, 0, -obj.TotalHeight), + ) + shape = shape.fuse(remove_channel) - hole_shape_sub_array = utils.copy_and_translate( - bottom_hole_shape, - utils.corners(x_hole_pos, y_hole_pos, -obj.TotalHeight), + shape = utils.copy_in_layout(shape, layout, obj.xGridSize, obj.yGridSize) + shape.translate( + fc.Vector(obj.xGridSize / 2 - obj.xLocationOffset, obj.yGridSize / 2 - obj.yLocationOffset), ) - fuse_total = utils.copy_in_layout(hole_shape_sub_array, layout, obj.xGridSize, obj.yGridSize) - fuse_total.translate(fc.Vector(obj.xGridSize / 2, obj.yGridSize / 2)) - return fuse_total.translate(fc.Vector(-obj.xLocationOffset, -obj.yLocationOffset)) + return shape def _stacking_lip_profile(obj: fc.DocumentObject) -> Part.Wire: diff --git a/freecad/gridfinity_workbench/init_gui.py b/freecad/gridfinity_workbench/init_gui.py index 5ee219a..935c90a 100644 --- a/freecad/gridfinity_workbench/init_gui.py +++ b/freecad/gridfinity_workbench/init_gui.py @@ -9,6 +9,8 @@ import FreeCAD as fc # noqa: N813 import FreeCADGui as fcg # noqa: N813 +from . import commands + try: from FreeCADGui import Workbench except ImportError: @@ -45,8 +47,6 @@ def Initialize(self) -> None: # noqa: N802 This function is called at the first activation of the workbench. here is the place to import all the commands. """ - from . import commands - fc.Console.PrintMessage("switching to Gridfinity Workbench\n") workbench_commands = OrderedDict( diff --git a/freecad/gridfinity_workbench/label_shelf.py b/freecad/gridfinity_workbench/label_shelf.py index ace85a1..593813c 100644 --- a/freecad/gridfinity_workbench/label_shelf.py +++ b/freecad/gridfinity_workbench/label_shelf.py @@ -26,7 +26,7 @@ def _corner_fillet(radius: float) -> Part.Face: Part.LineSegment(v3, v1), ] - return Part.Face(utils.curve_to_wire(lines)) + return utils.curve_to_face(lines) def outside_fillet( @@ -79,7 +79,7 @@ def from_dimensions( fc.Vector(0, yoffset, -height), ] - face = Part.Face(utils.curve_to_wire(utils.loop(v))) + face = utils.curve_to_face(utils.loop(v)) shape = face.extrude(fc.Vector(0, length)) # Front fillet diff --git a/freecad/gridfinity_workbench/magnet_hole.py b/freecad/gridfinity_workbench/magnet_hole.py new file mode 100644 index 0000000..419cccd --- /dev/null +++ b/freecad/gridfinity_workbench/magnet_hole.py @@ -0,0 +1,239 @@ +"""A module for making magnet holes.""" + +from __future__ import annotations + +import math + +import FreeCAD as fc # noqa: N813 +import Part + +from . import const, utils + +unitmm = fc.Units.Quantity("1 mm") + + +def add_properties( + obj: fc.DocumentObject, + *, + remove_channel: bool, + chamfer: bool, + magnet_holes_default: bool, +) -> None: + """Add magnet holes properties to an object. + + Args: + obj (FreeCAD.DocumentObject): Document object. + remove_channel (bool): Does the object support magnet remove channel. + chamfer (bool): Does the object support hole chamfer. + magnet_holes_default (bool): Should magnet holes be enabled by default. + + """ + ## Gridfinity Parameters + obj.addProperty( + "App::PropertyBool", + "MagnetHoles", + "Gridfinity", + "Toggle the magnet holes on or off", + ).MagnetHoles = magnet_holes_default + + ## Gridfinity Non Standard Parameters + obj.addProperty( + "App::PropertyLength", + "MagnetHoleDepth", + "GridfinityNonStandard", + "Depth of Magnet Holes

default = 2.4 mm", + ).MagnetHoleDepth = const.MAGNET_HOLE_DEPTH + + obj.addProperty( + "App::PropertyLength", + "MagnetHoleDiameter", + "GridfinityNonStandard", + ( + "Diameter of Magnet Holes. Press fit by default, increase to 6.5 mm if using glue." + "For crush ribs, 5.7mm is recommended.

default = 6.2 mm" + ), + ).MagnetHoleDiameter = const.MAGNET_HOLE_DIAMETER + + obj.addProperty( + "App::PropertyEnumeration", + "MagnetHolesShape", + "GridfinityNonStandard", + ( + "Shape of magnet holes, change to suit your printers capabilities which might require" + "testing." + "
Round is press fit by default, increase to 6.5 mm if using glue." + "

Crush ribs are an alternative press fit style." + "

Hex is a legacy press fit style." + ), + ).MagnetHolesShape = const.HOLE_SHAPES + + if chamfer: + obj.addProperty( + "App::PropertyLength", + "MagnetHoleChamfer", + "GridfinityNonStandard", + "The depth at which magnet hole chamfer starts.", + ).MagnetHoleChamfer = 0.25 + + if remove_channel: + obj.addProperty( + "App::PropertyBool", + "MagnetRemoveChannel", + "GridfinityNonStandard", + "Toggle the magnet remove channel on or off", + ).MagnetRemoveChannel = False + + obj.addProperty( + "App::PropertyInteger", + "CrushRibsCount", + "GridfinityNonStandard", + "Number of crush ribs

default = 12", + ).CrushRibsCount = const.CRUSH_RIB_N + + obj.addProperty( + "App::PropertyFloatConstraint", + "CrushRibsWaviness", + "GridfinityNonStandard", + "Waviness of crush ribs, from range [0, 1]", + ).CrushRibsWaviness = (const.CRUSH_RIB_WAVINESS, 0, 1, 0.05) + + ## Expert Only Parameters + obj.addProperty( + "App::PropertyLength", + "MagnetHoleDistanceFromEdge", + "zzExpertOnly", + "Distance of the magnet holes from bin edge

default = 8.0 mm", + read_only=True, + ).MagnetHoleDistanceFromEdge = const.MAGNET_HOLE_DISTANCE_FROM_EDGE + + +def _crush_ribs(radius: fc.Units.Quantity, *, n: int, beta: float) -> tuple[Part.Face, float]: + """Make crush ribs inner face. + + Args: + radius (fc.Units.Quantity): Inner radius. + n (int): Number of ribs. + beta (float): Waviness of the ribs. It is the angle at wich inner at outer arcs meet. + A value of 0 would result in a perfect circle. + + """ + alpha = math.pi / n / 2 + + def get_midpoint(beta: float) -> float: + return (math.sin(beta) - math.sin(alpha)) / math.sin(beta - alpha) + + r1 = radius.Value + r2 = radius.Value / get_midpoint(beta) + r3 = r2 * get_midpoint(-beta) + + p1 = fc.Vector(r2, 0) + p2 = fc.Vector(r2 * math.cos(2 * alpha), r2 * math.sin(2 * alpha)) + q = r1 * fc.Vector(math.cos(alpha), math.sin(alpha)) + r = r3 * fc.Vector(math.cos(alpha), math.sin(alpha)) + + lines = [] + for _ in range(n): + lines.append(Part.Arc(p1, q, p2)) + lines.append(Part.Arc(p1, r, p2)) + for i, arc in enumerate(lines): + placement = fc.Placement( + fc.Vector(0, 0, 0), + fc.Vector(0, 0, 1), + math.degrees((i + 0.5) / len(lines) * 2 * math.pi), + ) + arc.rotate(placement) + return utils.curve_to_face(lines), r3 - r1 + + +def _hex_shape(radius: fc.Units.Quantity) -> Part.Shape: + # Ratio of 2/sqrt(3) converts from inscribed circle radius to circumscribed + # circle radius + radius = 2 * radius / math.sqrt(3) + + p = fc.ActiveDocument.addObject("Part::RegularPolygon") + p.Polygon = 6 + p.Circumradius = radius + p.recompute() + + p_wire: Part.Wire = p.Shape + shape = Part.Face(p_wire) + fc.ActiveDocument.removeObject(p.Name) + return shape + + +def from_obj(obj: fc.DocumentObject) -> Part.Shape: + """Create a single magnet hole from object properties.""" + if not obj.MagnetHoles: + raise ValueError("Object doesn't have magnet holes enables") + + hole_shape = obj.MagnetHolesShape + radius = obj.MagnetHoleDiameter / 2 + depth = obj.MagnetHoleDepth + chamfer_depth = obj.MagnetHoleChamfer if hasattr(obj, "MagnetHoleChamfer") else None + + if hole_shape == "Hex": + shape = _hex_shape(radius) + chamfer_width = (2 / math.sqrt(3) - 1) * radius + elif hole_shape == "Crush ribs": + shape, ch = _crush_ribs( + radius, + n=obj.CrushRibsCount, + beta=obj.CrushRibsWaviness * math.pi / 2, + ) + chamfer_width = ch * unitmm + elif hole_shape == "Round": + shape = Part.Face(Part.Wire(Part.makeCircle(radius))) + chamfer_width = chamfer_depth + else: + raise ValueError(f"Unrecognised magnet hole shape {hole_shape:!r}") + + shape = shape.extrude(fc.Vector(0, 0, depth)) + + if obj.Baseplate: + assert chamfer_depth is not None + chamfer_shape = Part.makeCone( + radius, + radius + chamfer_width, + chamfer_depth, + fc.Vector(0, 0, depth - chamfer_depth), + ) + shape = shape.fuse(chamfer_shape) + + return shape + + +def remove_channel(obj: fc.DocumentObject) -> Part.Shape: + """Create a magnet remove channel shape for four magnets from object properties.""" + x_hole_pos = obj.xGridSize / 2 - obj.MagnetHoleDistanceFromEdge + y_hole_pos = obj.yGridSize / 2 - obj.MagnetHoleDistanceFromEdge + alpha = math.pi / 8 + + r = obj.MagnetHoleDiameter / 2 + x1 = r * math.cos(alpha) + y1 = r * math.sin(alpha) + p1 = fc.Vector(x1, y1) + p2 = fc.Vector(x1 + 2 * y1, y1) + p3 = fc.Vector(x1 + 3 * y1, 0) + p4 = fc.Vector(x1 + 2 * y1, -y1) + p5 = fc.Vector(x1, -y1) + lines = [ + Part.LineSegment(p1, p2), + Part.Arc(p2, p3, p4), + Part.LineSegment(p4, p5), + Part.LineSegment(p5, p1), + ] + face = utils.curve_to_face(lines) + shape = face.extrude(fc.Vector(0, 0, obj.MagnetHoleDepth)) + + positions = [ + (45, -x_hole_pos, -y_hole_pos), + (135, x_hole_pos, -y_hole_pos), + (225, x_hole_pos, y_hole_pos), + (315, -x_hole_pos, y_hole_pos), + ] + return utils.multi_fuse( + [ + shape.rotated(fc.Vector(0, 0, 0), fc.Vector(0, 0, 1), angle).translate(fc.Vector(x, y)) + for angle, x, y in positions + ], + ) diff --git a/freecad/gridfinity_workbench/test_gridfinity.py b/freecad/gridfinity_workbench/test_gridfinity.py index d230103..da14e12 100644 --- a/freecad/gridfinity_workbench/test_gridfinity.py +++ b/freecad/gridfinity_workbench/test_gridfinity.py @@ -182,9 +182,16 @@ def test_baseplate(self) -> None: def test_magnet_baseplate(self) -> None: fcg.Command.get("CreateMagnetBaseplate").run() obj = fcg.ActiveDocument.ActiveObject.Object - self.assertAlmostEqual(obj.Shape.Volume, 12606.095388468213) + self.assertAlmostEqual(obj.Shape.Volume, 12622.098661445636) + + def test_magnet_baseplate_hex(self) -> None: + fcg.Command.get("CreateMagnetBaseplate").run() + obj = fcg.ActiveDocument.ActiveObject.Object + obj.MagnetHolesShape = "Hex" + obj.recompute() + self.assertAlmostEqual(obj.Shape.Volume, 12502.10011889254) def test_screw_together_baseplate(self) -> None: fcg.Command.get("CreateScrewTogetherBaseplate").run() obj = fcg.ActiveDocument.ActiveObject.Object - self.assertAlmostEqual(obj.Shape.Volume, 22897.352081257995) + self.assertAlmostEqual(obj.Shape.Volume, 22913.35535423545) diff --git a/freecad/gridfinity_workbench/utils.py b/freecad/gridfinity_workbench/utils.py index 0408be9..dd14416 100644 --- a/freecad/gridfinity_workbench/utils.py +++ b/freecad/gridfinity_workbench/utils.py @@ -115,23 +115,22 @@ def copy_in_grid( return multi_fuse(shapes) -def curve_to_wire(list_of_items: Sequence[Part.TrimmedCurve]) -> Part.Wire: - """Make a wire from curves (line,linesegment,arc,ect). +def curve_to_face(curves: Sequence[Part.TrimmedCurve]) -> Part.Face: + """Make a face from curves (line,linesegment,arc,ect). - This function accepts all curves and makes it into a wire. Note that the wire should be - closed. + Note that the sequence should be a closed curve -- the end of last curve should be the beginning + of the first one. Args: - list_of_items (list[Part.LineSegment]): List of items to convert in a wire. - - Returns: - Part.Wire: The created wire. + curves (Sequence[Part.LineSegment]): Sequence of corves to that enclose the face. """ - if not list_of_items: + if not curves: raise ValueError("List is empty") + if not curves[0].StartPoint.isEqual(curves[-1].EndPoint, 1e-5): + raise ValueError("The sequence is not a closed curve") - return Part.Wire([item.toShape() for item in list_of_items]) + return Part.Face(Part.Wire([item.toShape() for item in curves])) def create_rounded_rectangle( diff --git a/tests/test_unit_utils.py b/tests/test_unit_utils.py index 5b92117..9ba9ea7 100644 --- a/tests/test_unit_utils.py +++ b/tests/test_unit_utils.py @@ -63,29 +63,8 @@ def test_copy_in_grid(self) -> None: ) self.assertEqual(shapes, shape.translated().multiFuse()) - def test_curve_to_wire_empty_list(self) -> None: - self.assertRaises(ValueError, utils.curve_to_wire, []) - - def test_curve_to_wire_one_line(self) -> None: - vertexes = [fc.Vector(0, 0, 0), fc.Vector(10, 0, 0)] - line = Part.LineSegment(vertexes[0], vertexes[1]) - - wire = utils.curve_to_wire([line]) - - self.assertEqual(wire.Length, 10) - self.assertListEqual(vertexes, [vertex.Point for vertex in wire.Vertexes]) - - def test_cuve_to_wire_rectangle(self) -> None: - line_1 = Part.LineSegment(fc.Vector(0, 0, 0), fc.Vector(10, 0, 0)) - line_2 = Part.LineSegment(fc.Vector(10, 0, 0), fc.Vector(10, 10, 0)) - line_3 = Part.LineSegment(fc.Vector(10, 10, 0), fc.Vector(10, 0, 0)) - line_4 = Part.LineSegment(fc.Vector(10, 0, 0), fc.Vector(0, 0, 0)) - - wire = utils.curve_to_wire([line_1, line_2, line_3, line_4]) - - self.assertTrue(wire.isClosed) - self.assertEqual(wire.Length, 10 * 4) - self.assertEqual(len(wire.Edges), 4) + def test_curve_to_face_empty_list(self) -> None: + self.assertRaises(ValueError, utils.curve_to_face, []) def test_create_rounded_rectangle_radius_1(self) -> None: length, width, radius = 5, 6, 1