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