diff --git a/README.md b/README.md
index 507b047..6080861 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,11 @@ A place to share CadQuery scripts, modules, tutorials and projects
+* [Hexagonal modular drawers](examples/hexagonal_drawers/assembly.py) - Inspired by [this on Prusa Printers](https://www.prusaprinters.org/prints/54113-hexagonal-organizer-system), these drawers are 3D printed (without needing supports) and clip together.
+
+
+
+
### Tutorials
* [Ex000 Start Here.ipynb](tutorials/Ex000%20Start%20Here.ipynb) - iPython notebook that is the entry point for a set of CadQuery tutorials
diff --git a/examples/hexagonal_drawers/assembly.py b/examples/hexagonal_drawers/assembly.py
new file mode 100644
index 0000000..9fb503b
--- /dev/null
+++ b/examples/hexagonal_drawers/assembly.py
@@ -0,0 +1,74 @@
+"""
+Display the drawers
+"""
+
+import cadquery as cq
+import importlib
+import base
+import organiser_collets
+import organiser_3_125_bits
+importlib.reload(base)
+importlib.reload(organiser_collets)
+importlib.reload(organiser_3_125_bits)
+
+sep = 20 # seperation between parts
+big_sep = 100 # extra for the organisers
+
+assy = cq.Assembly()
+assy.add(base.frame, name="frame")
+assy.add(base.drawer, name="drawer")
+assy.add(organiser_collets.collet_organiser, name="collet organiser")
+assy.add(organiser_3_125_bits.bit_organiser, name="bit organiser")
+
+# pull the drawer out
+assy.constrain(
+ "frame",
+ base.frame.faces("Y").val(),
+ "Point",
+)
+
+# bit organiser aligns with back face of drawer and is big_sep above
+assy.constrain(
+ "drawer",
+ base.drawer.faces("Y").val(),
+ "Point",
+)
+assy.constrain(
+ "bit organiser",
+ organiser_3_125_bits.bit_organiser.faces("Y").val(),
+ "Point",
+)
+
+# align the z and x axes between obj0 and obj1
+align_these = (
+ ("frame", "drawer"),
+ ("drawer", "collet organiser"),
+ ("drawer", "bit organiser"),
+)
+for obj0, obj1 in align_these:
+ assy.constrain(
+ obj0,
+ cq.Face.makePlane(),
+ obj1,
+ cq.Face.makePlane(),
+ "Axis",
+ 0,
+ )
+ assy.constrain(
+ obj0,
+ cq.Face.makePlane(dir=(1, 0, 0)),
+ obj1,
+ cq.Face.makePlane(dir=(1, 0, 0)),
+ "Axis",
+ 0,
+ )
+
+assy.solve()
+if "show_object" in locals():
+ show_object(assy)
diff --git a/examples/hexagonal_drawers/base.py b/examples/hexagonal_drawers/base.py
new file mode 100644
index 0000000..d1a7814
--- /dev/null
+++ b/examples/hexagonal_drawers/base.py
@@ -0,0 +1,119 @@
+"""
+The basic frame and drawer.
+"""
+
+import cadquery as cq
+from types import SimpleNamespace
+from math import tan, radians
+
+hex_diam = 80 # outside of the drawer frame
+wall_thick = 3
+clearance = SimpleNamespace(tight=0.3)
+clearance.loose = clearance.tight * 2
+drawer_length = 150
+dovetail_min_thick = wall_thick * 2
+
+frame_y = drawer_length + clearance.loose + 2 * wall_thick
+
+frame = (
+ cq.Workplane("XZ")
+ .polygon(6, hex_diam)
+ .extrude(frame_y)
+ .faces("Z or >>Z[-2]")
+ .shell(-wall_thick)
+)
+
+handle = (
+ drawer
+ .faces("Z").val().BoundingBox().ylen
+dovetail_base_radius = frame.faces("Z").val().Center().z
+dovetail_length = 0.9 * top_length
+
+# make the male dovetail join
+# should extend wall_thick out from the frame
+dovetail_positive = (
+ cq.Workplane()
+ .hLine(dovetail_min_thick / 2)
+ .line(wall_thick * tan(radians(30)), wall_thick)
+ .hLineTo(0)
+ .mirrorY()
+ .extrude(-dovetail_length)
+ .faces("Y and |Z").val().Length()
+dovetail_negative = (
+ dovetail_positive
+ .tag("dovetail_positive")
+ .faces(">Z")
+ .wires()
+ .toPending()
+ .offset2D(clearance.tight)
+ .faces(">Z", tag="dovetail_positive")
+ .workplane()
+ .extrude(-(dovetail_length + clearance.tight))
+ .faces("Z")
+ .workplane(centerOption="CenterOfMass")
+)
+
+bit_3_125_points = (
+ base_wp
+ .rarray(
+ 3.125 * 3,
+ 3.125 * 4,
+ 4,
+ 4,
+ )
+ .vals()
+)
+bit_3_125_points.extend(
+ base_wp
+ .rarray(
+ 3.125 * 3,
+ 3.125 * 4,
+ 3,
+ 3,
+ )
+ .vals()
+)
+bit_organiser = (
+ base_wp
+ .newObject(bit_3_125_points)
+ .hole(
+ 3.125 + 2 * base.clearance.loose,
+ depth=organiser_blank.organiser.val().BoundingBox().zlen - 1.5 * base.wall_thick,
+ )
+)
diff --git a/examples/hexagonal_drawers/organiser_blank.py b/examples/hexagonal_drawers/organiser_blank.py
new file mode 100644
index 0000000..688cefa
--- /dev/null
+++ b/examples/hexagonal_drawers/organiser_blank.py
@@ -0,0 +1,37 @@
+"""
+An organiser that takes up 1/3 of a drawer and has no cutouts yet.
+"""
+
+import cadquery as cq
+import importlib
+import base
+importlib.reload(base)
+
+
+# organiser base
+# first grab the inner profile of the drawer
+lines = (
+ base.drawer
+ .faces(">Y")
+ .workplane(offset=-base.drawer_length / 2)
+ .section()
+ .edges()
+ .vals()
+)
+assert len(lines) == 8
+# sort lines by radius about y axis
+lines.sort(
+ key=lambda x: cq.Vector(x.Center().x, 0, x.Center().z).Center().Length
+)
+wire = cq.Wire.assembleEdges(lines[0:3])
+wire = cq.Wire.combine(
+ [wire, cq.Edge.makeLine(wire.endPoint(), wire.startPoint())]
+)[0]
+wire_center = wire.Center()
+wire = wire.translate(cq.Vector(0, -wire_center.y, 0))
+organiser = (
+ cq.Workplane("XZ")
+ .newObject([wire])
+ .toPending()
+ .extrude(base.drawer_length / 3 - base.clearance.loose)
+)
diff --git a/examples/hexagonal_drawers/organiser_collets.py b/examples/hexagonal_drawers/organiser_collets.py
new file mode 100644
index 0000000..473d02e
--- /dev/null
+++ b/examples/hexagonal_drawers/organiser_collets.py
@@ -0,0 +1,61 @@
+"""
+An organiser with holes for ER11 collets.
+"""
+
+import cadquery as cq
+from types import SimpleNamespace
+import importlib
+import organiser_blank
+importlib.reload(organiser_blank)
+
+
+collet_dims = SimpleNamespace(
+ upper_diam=11.35,
+ cone_height=13.55,
+ lower_diam=7.8,
+)
+collet = (
+ cq.Solid.makeCone(
+ collet_dims.upper_diam / 2,
+ collet_dims.lower_diam / 2,
+ collet_dims.cone_height,
+ )
+ .mirror("XY")
+ .translate(cq.Vector(0, 0, collet_dims.cone_height / 3))
+)
+collet_organiser_points = (
+ organiser_blank.organiser
+ .faces(">Z")
+ .workplane(centerOption="CenterOfMass")
+ .rarray(
+ collet_dims.upper_diam * 1.5,
+ collet_dims.upper_diam * 2.3,
+ 3,
+ 2,
+ )
+ .vals()
+)
+collet_organiser_points.extend(
+ organiser_blank.organiser
+ .faces(">Z")
+ .workplane(centerOption="CenterOfMass")
+ .rarray(
+ collet_dims.upper_diam * 1.5,
+ collet_dims.upper_diam * 2.3,
+ 2,
+ 1,
+ )
+ .vals()
+)
+collets = (
+ cq.Workplane()
+ .pushPoints(collet_organiser_points)
+ .eachpoint(lambda loc: collet.located(loc))
+)
+collet_organiser = (
+ organiser_blank.organiser
+ .cut(collets)
+)
+
+if "show_object" in locals():
+ show_object(collet_organiser, "collet organiser")