Konubinix' opinionated web of thoughts

Modular Snap Together Bins

Fleeting

tag: learning 3D printing

Looking for free, open-source modular 3D printable bins that snap together side-by-side, like MultiBin from MultiBuild but free. Gridfinity requires a baseplate which is not suitable here.

Inspired by Jujumo’s interlocking and stackable containers V3, we designed a parametric system in build123d with Claude.

The parameters are calibrated to fit well with a 0.8mm nozzle on a bambulab a1 mini.

Vase mode printing

The bins are designed for vase mode (aka spiral/contour mode). In vase mode, the slicer prints a single continuous spiral — no retractions, no seams. The model must therefore be a solid block: the slicer itself creates the thin wall by tracing the outer contour. This means we extrude a solid shape and let the slicer hollow it out, rather than modeling walls explicitly.

This constrains the geometry: everything must be a single continuous shell with no internal features, overhangs that would need support, or disconnected volumes.

Snap-fit interlocking

The bins interlock by sliding together vertically. Primary walls (front, left) carry bumps at grid-unit centers (0.5) and boundaries (0.0, 1.0, …), while secondary walls (back, right) carry 2 bumps per grid unit (at 0.25 and 0.75). When two bins are placed side by side, each primary bump slides between a pair on the facing wall.

Once interleaved, the bumps physically prevent horizontal separation: each primary bump sits between a pair of secondary bumps, so the bins cannot be pulled apart without the solids passing through each other. The bins can only be assembled or disassembled by sliding vertically.

Interleaving constraint: the sub-cell width is GRID_UNIT / 2 (the spacing between adjacent bumps on the secondary wall). The bump tip width fills this gap: tip_w = sub - fw - 2 * CLEARANCE.

Vertical stacking

The bins stack vertically by nesting the bottom of one bin into the top of another. The bottom STACK_HEIGHT mm of each bin is a nesting lip — inset by NOZZLE_SIZE + STACK_CLEARANCE per side so it fits inside the inner cavity of the bin below. In vase mode the wall thickness equals the nozzle diameter (0.8 mm), so the inner cavity is outer - 2 * NOZZLE_SIZE per axis. The lip is inset by NOZZLE_SIZE + STACK_CLEARANCE per side, leaving STACK_CLEARANCE (0.15 mm) of play for an easy fit. The dovetail bumps start above the lip so they don’t interfere with stacking.

Inner volume trade-off: in vase mode the wall thickness is fixed at NOZZLE_SIZE, so the smaller outer of the lip also shrinks the inner cavity by NOZZLE_SIZE + STACK_CLEARANCE per side at the bottom. The lip is built as a loft from the smaller footprint at Z=0 to the full body size at Z==STACK_HEIGHT=, giving a smooth taper that slices cleanly in vase mode (no abrupt step).

Parameters

The design is fully parametric. The sub-cell width sub = GRID_UNIT / 2 defines the bump spacing. Finger ratio controls the base width of each finger. The tip width is derived as tip_w = sub - fw - 2 * CLEARANCE so the dovetail exactly fills the gap between adjacent bumps on the facing wall, with CLEARANCE per side.

GRID_UNIT = 10.0        # mm per grid unit
FINGER_RATIO = 0.3      # fraction of sub-cell used for finger base width
FINGER_DEPTH = 5.0      # mm, how far bumps protrude
CLEARANCE = 0.12        # mm, per-side clearance between interleaving bumps
NOZZLE_SIZE = 0.8       # mm, nozzle diameter (wall thickness in vase mode)
STACK_HEIGHT = 3.0      # mm, height of the nesting lip at the bottom
STACK_CLEARANCE = 0.15  # mm, extra clearance for stacking fit

Grid-aligned fingers

Secondary walls get 2 fingers per GRID_UNIT (at sub-cell positions 0.25 and 0.75). Primary walls get 1 finger at the center (0.5) plus 1 at each grid-unit boundary (0.0) — the boundary bumps fill the gap between secondary pairs of adjacent grid units in multi-unit bins; bumps at the wall edges are omitted by the corner constraint. Each finger width is sub * FINGER_RATIO, leaving flat margins on each side. This grid alignment guarantees that bins of any size snap together: a 1×1 bin’s fingers align with the corresponding region of a 3×2 bin’s wall, because every finger sits at the same absolute grid position.

Corner constraint: bumps on one edge must never protrude past the adjacent wall surface. At each corner, two perpendicular walls meet and both carry bumps that protrude outward. A bump whose tip-width footprint extends past the neighboring wall surface would collide with that wall’s bumps. Therefore, for front/back wall bumps at position x, we require x - tip_w/2 > -ow/2= and x + tip_w/2 < ow/2=; likewise for left/right wall bumps along y. Any bump violating this is simply omitted.

Dovetail profile

The bump is a trapezoidal dovetail profile. The base sits at the wall (width finger_width), overlapping 0.1 mm into the box for a clean boolean union; the tip protrudes outward and is wider (tip_w = sub - fw - 2 * CLEARANCE). The taper fills the gap between adjacent bumps on the facing wall with uniform clearance at every depth.

To avoid the elephant foot effect (first layer squished wider by bed adhesion), the bumps do not touch the build plate. Each bump has a 45° ramp at the bottom: at Z=0 the profile is flush with the wall (no protrusion), and it linearly reaches full protrusion at Z==FINGER_DEPTH=. This slope is gentle enough to print without support. The ramp is built by lofting from a thin wall-flush strip at Z=0 to the full dovetail profile at Z==FINGER_DEPTH=, then extruding the full profile from there to the top.

The profile is parameterized by direction (-x, +x, -y, +y) so the same function works for all four walls.

def _make_dovetail(base_w, tip_w, depth, height, direction):
    slope_height = depth  # 45° ramp: flush at Z=0, full protrusion at Z=slope_height
    half_base = base_w / 2
    half_tip = tip_w / 2
    overlap = 0.1  # overlap into wall for clean boolean union
    if direction == "-y":
        full_pts = [(-half_base, overlap), (-half_tip, -depth), (half_tip, -depth), (half_base, overlap)]
        base_pts = [(-half_base, overlap), (-half_base, 0), (half_base, 0), (half_base, overlap)]
    elif direction == "+y":
        full_pts = [(-half_base, -overlap), (-half_tip, depth), (half_tip, depth), (half_base, -overlap)]
        base_pts = [(-half_base, -overlap), (-half_base, 0), (half_base, 0), (half_base, -overlap)]
    elif direction == "-x":
        full_pts = [(overlap, -half_base), (-depth, -half_tip), (-depth, half_tip), (overlap, half_base)]
        base_pts = [(overlap, -half_base), (0, -half_base), (0, half_base), (overlap, half_base)]
    elif direction == "+x":
        full_pts = [(-overlap, -half_base), (depth, -half_tip), (depth, half_tip), (-overlap, half_base)]
        base_pts = [(-overlap, -half_base), (0, -half_base), (0, half_base), (-overlap, half_base)]
    # Main body: full profile from slope_height to height
    with BuildSketch(Plane.XY.offset(slope_height)) as full_sketch:
        with BuildLine():
            Polyline(*full_pts, close=True)
        make_face()
    main_body = extrude(full_sketch.sketch, height - slope_height)
    # Ramp: loft from wall-flush base at Z=0 to full profile at Z=slope_height
    with BuildPart() as ramp_part:
        with BuildSketch(Plane.XY) as base_sketch:
            with BuildLine():
                Polyline(*base_pts, close=True)
            make_face()
        with BuildSketch(Plane.XY.offset(slope_height)) as top_sketch:
            with BuildLine():
                Polyline(*full_pts, close=True)
            make_face()
        loft()
    return main_body + ramp_part.part

Assembling the bin

The bin starts as a solid box sized GRID_UNIT * n in each dimension (remember: vase mode hollows it). Bumps protrude FINGER_DEPTH beyond each wall; when two bins are placed adjacent with a gap of FINGER_DEPTH, bumps from each side fully interleave. The center-to-center pitch is GRID_UNIT + FINGER_DEPTH.

All four walls get dovetail bumps (union only, no subtraction). Primary walls (front -y, left -x) place bumps at positions 0.0 and 0.5 per grid unit, and secondary walls (back +y, right +x) place 2 bumps at positions 0.25 and 0.75. Each primary bump lands in the gap between a pair on the facing wall.

def make_bin(width_units=1, depth_units=1, height=50.0):
    outer_width = width_units * GRID_UNIT
    outer_depth = depth_units * GRID_UNIT
    subcell = GRID_UNIT / 2  # sub-cell size (2 bumps per unit on secondary walls)
    finger_width = subcell * FINGER_RATIO
    finger_depth = FINGER_DEPTH
    tip_width = subcell - finger_width - 2 * CLEARANCE  # fills gap between facing-wall bumps
    stack_inset = NOZZLE_SIZE + STACK_CLEARANCE  # per-side inset for nesting lip
    # Bottom nesting lip: loft from smaller footprint at Z=0 to full size at Z=STACK_HEIGHT
    with BuildPart() as lip_part:
        with BuildSketch(Plane.XY):
            Rectangle(outer_width - 2 * stack_inset, outer_depth - 2 * stack_inset)
        with BuildSketch(Plane.XY.offset(STACK_HEIGHT)):
            Rectangle(outer_width, outer_depth)
        loft()
    # Main body at full size, starting above the lip
    body = Pos(0, 0, STACK_HEIGHT) * Box(outer_width, outer_depth, height - STACK_HEIGHT,
              align=(Align.CENTER, Align.CENTER, Align.MIN))
    bin_solid = lip_part.part + body
    # Chamfer the vertical corner edges
    vertical_edges = bin_solid.edges().filter_by(Axis.Z)
    bin_solid = chamfer(vertical_edges, length=0.5)

    primary_fracs = [0.0, 0.5]   # center + unit boundary (edge ones filtered by corner constraint)
    secondary_fracs = [0.25, 0.75]  # 2 bumps per grid unit

    def _add_bumps(fracs, units, wall_pos, direction, along_wall):
        nonlocal bin_solid
        half = along_wall / 2
        for u in range(units):
            for frac in fracs:
                center = -half + GRID_UNIT * (u + frac)
                if center - tip_width / 2 < -half or center + tip_width / 2 > half:
                    continue  # corner constraint
                bump = _make_dovetail(finger_width, tip_width, finger_depth, height - STACK_HEIGHT, direction)
                bin_solid = bin_solid + wall_pos(center) * bump

    # Front wall (primary, -y) — bumps start above the nesting lip
    _add_bumps(primary_fracs, width_units, lambda x: Pos(x, -outer_depth / 2, STACK_HEIGHT), "-y", outer_width)
    # Back wall (secondary, +y)
    _add_bumps(secondary_fracs, width_units, lambda x: Pos(x, outer_depth / 2, STACK_HEIGHT), "+y", outer_width)
    # Left wall (primary, -x)
    _add_bumps(primary_fracs, depth_units, lambda y: Pos(-outer_width / 2, y, STACK_HEIGHT), "-x", outer_depth)
    # Right wall (secondary, +x)
    _add_bumps(secondary_fracs, depth_units, lambda y: Pos(outer_width / 2, y, STACK_HEIGHT), "+x", outer_depth)

    if isinstance(bin_solid, _b123d.topology.shape_core.ShapeList):
        return max(bin_solid, key=lambda s: s.volume)
    return bin_solid

Generating the STL

Notes linking here