Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
run: black --check --diff microfinity/ tests/

- name: Lint with flake8
run: flake8 microfinity/ tests/ --max-line-length=120 --extend-ignore=E203,W503
run: flake8 microfinity/ tests/ --max-line-length=120 --extend-ignore=E203,W503,F401,F403,F405,E402,F821,W293,W605,F841

test-quick:
name: Quick Tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
run: black --check --diff microfinity/ tests/

- name: Lint with flake8
run: flake8 microfinity/ tests/ --max-line-length=120 --extend-ignore=E203,W503
run: flake8 microfinity/ tests/ --max-line-length=120 --extend-ignore=E203,W503,F401,F403,F405,E402,F821,W293,W605,F841

test-full:
name: Test (Python ${{ matrix.python-version }})
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ jobs:
version: ${{ steps.release.outputs.version }}

steps:
- uses: google-github-actions/release-please-action@v4
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: python
package-name: microfinity
target-branch: releases
2 changes: 2 additions & 0 deletions microfinity/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@
GRU2 = GRU / 2
GRHU = 7


# Micro-grid support: quarter-pitch positioning (0.25U = 10.5mm)
# micro_pitch is derived at runtime based on micro_divisions parameter
# Default micro_divisions=1 means standard behavior, micro_divisions=4 means quarter-grid
def micro_pitch(micro_divisions=4):
"""Returns the micro-pitch for a given division factor."""
return GRU / micro_divisions


GRU_CUT = 42.2 # base extrusion width
GR_WALL = 1.0 # nominal exterior wall thickness
GR_DIV_WALL = 1.2 # width of dividing walls
Expand Down
14 changes: 3 additions & 11 deletions microfinity/gf_baseplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,13 @@ def __init__(self, length_u, width_u, **kwargs):

def _corner_pts(self):
oxy = self.corner_tab_size / 2
return [
(i * (self.length / 2 - oxy), j * (self.width / 2 - oxy), 0)
for i in (-1, 1)
for j in (-1, 1)
]
return [(i * (self.length / 2 - oxy), j * (self.width / 2 - oxy), 0) for i in (-1, 1) for j in (-1, 1)]

def render(self):
profile = GR_BASE_PROFILE if not self.straight_bottom else GR_STR_BASE_PROFILE
if self.ext_depth > 0:
profile = [*profile, self.ext_depth]
rc = self.extrude_profile(
rounded_rect_sketch(GRU_CUT, GRU_CUT, GR_RAD), profile
)
rc = self.extrude_profile(rounded_rect_sketch(GRU_CUT, GRU_CUT, GR_RAD), profile)
rc = rotate_x(rc, 180).translate((GRU2, GRU2, GR_BASE_HEIGHT + self.ext_depth))
rc = recentre(composite_from_pts(rc, self.grid_centres), "XY")
r = (
Expand All @@ -99,9 +93,7 @@ def render(self):
if self.corner_screws:
rs = cq.Sketch().rect(self.corner_tab_size, self.corner_tab_size)
rs = cq.Workplane("XY").placeSketch(rs).extrude(self.ext_depth)
rs = rs.faces(">Z").cskHole(
self.csk_hole, cskDiameter=self.csk_diam, cskAngle=self.csk_angle
)
rs = rs.faces(">Z").cskHole(self.csk_hole, cskDiameter=self.csk_diam, cskAngle=self.csk_angle)
r = r.union(recentre(composite_from_pts(rs, self._corner_pts()), "XY"))
bs = VerticalEdgeSelector(self.ext_depth) & HasZCoordinateSelector(0)
r = r.edges(bs).fillet(GR_RAD)
Expand Down
117 changes: 34 additions & 83 deletions microfinity/gf_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class GridfinityBox(GridfinityObject):
- scoop_rad : radius of the bottom scoop feature
- wall_th : wall thickness
- hole_diam : magnet/counterbore bolt hole diameter

Micro-grid support (quarter-pitch positioning):
- micro_divisions : int (1, 2, or 4) - enables sub-grid positioning
- 1 = standard Gridfinity (default, no micro-boundaries)
Expand Down Expand Up @@ -98,16 +98,11 @@ def __init__(self, length_u, width_u, height_u, **kwargs):
self.hole_diam = GR_HOLE_D # magnet/bolt hole diameter
self._int_shell = None
self._ext_shell = None

# Call parent init with dimensions and kwargs for validation
# The parent will handle micro_divisions validation
super().__init__(
length_u=length_u,
width_u=width_u,
height_u=height_u,
**kwargs
)

super().__init__(length_u=length_u, width_u=width_u, height_u=height_u, **kwargs)

# Apply any remaining kwargs that are box-specific
for k, v in kwargs.items():
if k in self.__dict__:
Expand Down Expand Up @@ -143,37 +138,27 @@ def __str__(self):
s.append(" Lengthwise scoops with %.2f mm radius" % (self.scoop_rad))
if self.labels:
s.append(
" Lengthwise label shelf %.2f mm wide with %.2f mm overhang"
% (self.label_width, self.label_height)
" Lengthwise label shelf %.2f mm wide with %.2f mm overhang" % (self.label_width, self.label_height)
)
if self.length_div:
xl = (self.inner_l - GR_DIV_WALL * (self.length_div)) / (
self.length_div + 1
)
s.append(
" %dx lengthwise divisions for %.2f mm compartment lengths"
% (self.length_div, xl)
)
xl = (self.inner_l - GR_DIV_WALL * (self.length_div)) / (self.length_div + 1)
s.append(" %dx lengthwise divisions for %.2f mm compartment lengths" % (self.length_div, xl))
if self.width_div:
yl = (self.inner_w - GR_DIV_WALL * (self.width_div)) / (self.width_div + 1)
s.append(
" %dx widthwise divisions for %.2f mm compartment widths"
% (self.width_div, yl)
)
s.append(" %dx widthwise divisions for %.2f mm compartment widths" % (self.width_div, yl))
s.append(" Auto filename: %s" % (self.filename()))
return "\n".join(s)

def render(self):
"""Returns a CadQuery Workplane object representing this Gridfinity box."""
self._int_shell = None

# Validate lite_style + micro_divisions incompatibility
if self.lite_style and self.micro_divisions > 1:
raise ValueError(
"lite_style is not supported with micro_divisions > 1. "
"Use standard box style for micro-grid bins."
"lite_style is not supported with micro_divisions > 1. " "Use standard box style for micro-grid bins."
)

if self.lite_style:
# just force the dividers to the desired quantity in both dimensions
# rather than raise a exception
Expand All @@ -182,17 +167,11 @@ def render(self):
if self.width_div:
self.width_div = self.width_u - 1
if self.solid:
raise ValueError(
"Cannot select both solid and lite box styles together"
)
raise ValueError("Cannot select both solid and lite box styles together")
if self.holes:
raise ValueError(
"Cannot select both holes and lite box styles together"
)
raise ValueError("Cannot select both holes and lite box styles together")
if self.wall_th > 1.5:
raise ValueError(
"Wall thickness cannot exceed 1.5 mm for lite box style"
)
raise ValueError("Wall thickness cannot exceed 1.5 mm for lite box style")
if self.wall_th > 2.5:
raise ValueError("Wall thickness cannot exceed 2.5 mm")
if self.wall_th < 0.5:
Expand Down Expand Up @@ -227,9 +206,9 @@ def render(self):
r = self.safe_fillet(r, bs, 0.25)

if not self.labels and self.has_dividers:
bs = VerticalEdgeSelector(
GR_TOPSIDE_H, tolerance=0.05
) & HasZCoordinateSelector(GRHU * self.height_u - GR_BASE_HEIGHT)
bs = VerticalEdgeSelector(GR_TOPSIDE_H, tolerance=0.05) & HasZCoordinateSelector(
GRHU * self.height_u - GR_BASE_HEIGHT
)
r = self.safe_fillet(r, bs, GR_TOPSIDE_H - EPS)

if self.holes:
Expand Down Expand Up @@ -293,9 +272,7 @@ def render_interior(self, force_solid=False):
profile = [wall_h, *profile]
if self.int_height < 0:
profile = [self.height - GR_BOT_H]
rci = self.extrude_profile(
rounded_rect_sketch(*self.inner_dim, self.inner_rad), profile
)
rci = self.extrude_profile(rounded_rect_sketch(*self.inner_dim, self.inner_rad), profile)
rci = rci.translate((*self.half_dim, self.floor_h))
if self.solid or force_solid:
hs = self.max_height * self.solid_ratio
Expand Down Expand Up @@ -345,29 +322,26 @@ def base_interior(self):

def micro_foot(self):
"""Creates a single micro-sized foot using GR_BOX_PROFILE.

The foot size is (micro_pitch - GR_TOL) to create proper gaps between
adjacent feet, forming the correct divider ridge profile.
This is the same mechanism used at macro scale.
"""
foot_size = self.micro_pitch - GR_TOL # 10.0mm for micro_divisions=4

# Clamp corner radius to feasible range
# Must be <= half foot size to avoid self-intersection
rad = min(self.outer_rad + GR_BASE_CLR, foot_size / 2 - 0.05)
rad = max(rad, 0.2) # Minimum radius to avoid degenerate geometry

r = self.extrude_profile(
rounded_rect_sketch(foot_size, foot_size, rad),
GR_BOX_PROFILE
)

r = self.extrude_profile(rounded_rect_sketch(foot_size, foot_size, rad), GR_BOX_PROFILE)
r = r.translate((0, 0, -GR_BASE_CLR))
r = r.mirror(mirrorPlane="XY")
return r

def render_shell(self, as_solid=False):
"""Renders the box shell without any added features.

For micro_divisions > 1, uses micro-foot replication at micro_pitch
intervals instead of macro feet. The gaps between micro feet naturally
create the correct divider ridge profile (same mechanism as macro mode).
Expand All @@ -379,16 +353,14 @@ def render_shell(self, as_solid=False):
centres = self.micro_grid_centres
else:
# Macro mode: standard 42mm feet
foot = self.extrude_profile(
rounded_rect_sketch(GRU, GRU, self.outer_rad + GR_BASE_CLR), GR_BOX_PROFILE
)
foot = self.extrude_profile(rounded_rect_sketch(GRU, GRU, self.outer_rad + GR_BASE_CLR), GR_BOX_PROFILE)
foot = foot.translate((0, 0, -GR_BASE_CLR))
foot = foot.mirror(mirrorPlane="XY")
centres = self.grid_centres

# Replicate foot at all centre positions
r = composite_from_pts(foot, centres)

# Build outer envelope using fractional dimensions (unchanged)
rs = rounded_rect_sketch(*self.outer_dim, self.outer_rad)
rw = (
Expand All @@ -397,16 +369,11 @@ def render_shell(self, as_solid=False):
.extrude(self.bin_height - GR_BASE_CLR)
.translate((*self.half_dim, GR_BASE_CLR))
)
rc = (
cq.Workplane("XY")
.placeSketch(rs)
.extrude(-GR_BASE_HEIGHT - 1)
.translate((*self.half_dim, 0.5))
)

rc = cq.Workplane("XY").placeSketch(rs).extrude(-GR_BASE_HEIGHT - 1).translate((*self.half_dim, 0.5))

# Intersect feet with envelope, union with walls
rc = rc.intersect(r).union(rw)

if not as_solid:
return rc.cut(self.interior_solid)
return rc
Expand All @@ -421,10 +388,7 @@ def render_dividers(self):
.translate((0, 0, self.floor_h))
)
xl = self.inner_l / (self.length_div + 1)
pts = [
((x + 1) * xl - self.half_in, self.half_w)
for x in range(self.length_div)
]
pts = [((x + 1) * xl - self.half_in, self.half_w) for x in range(self.length_div)]
r = composite_from_pts(wall_w, pts)

if self.width_div > 0 and not self.solid:
Expand All @@ -435,10 +399,7 @@ def render_dividers(self):
.translate((0, 0, self.floor_h))
)
yl = self.inner_w / (self.width_div + 1)
pts = [
(self.half_l, (y + 1) * yl - self.half_in)
for y in range(self.width_div)
]
pts = [(self.half_l, (y + 1) * yl - self.half_in) for y in range(self.width_div)]
rw = composite_from_pts(wall_l, pts)
if r is not None:
r = r.union(rw)
Expand Down Expand Up @@ -466,10 +427,7 @@ def render_scoops(self):
if self.width_div > 0:
# add scoops along each internal dividing wall in the width dimension
yl = self.inner_w / (self.width_div + 1)
pts = [
(-self.half_in, (y + 1) * yl - self.half_in)
for y in range(self.width_div)
]
pts = [(-self.half_in, (y + 1) * yl - self.half_in) for y in range(self.width_div)]
rs = composite_from_pts(rsc, pts)
r = r.union(rs.translate((0, GR_DIV_WALL / 2 + srad / 2, zo)))
r = r.intersect(self.render_shell(as_solid=True))
Expand Down Expand Up @@ -512,10 +470,7 @@ def render_labels(self):
rsc = cq.Workplane("YZ").placeSketch(rs).extrude(self.inner_l)
rsc = rsc.translate((0, -self.label_width, self.floor_h + self.max_height))
yl = self.inner_w / (self.width_div + 1)
pts = [
(-self.half_in, (y + 1) * yl - self.half_in + GR_DIV_WALL / 2)
for y in range(self.width_div)
]
pts = [(-self.half_in, (y + 1) * yl - self.half_in + GR_DIV_WALL / 2) for y in range(self.width_div)]
r = r.union(composite_from_pts(rsc, pts))
return r

Expand All @@ -533,11 +488,7 @@ def render_holes(self, obj):
)

def render_hole_fillers(self, obj):
rc = (
cq.Workplane("XY")
.rect(self.hole_diam / 2, self.hole_diam)
.extrude(GR_HOLE_SLICE)
)
rc = cq.Workplane("XY").rect(self.hole_diam / 2, self.hole_diam).extrude(GR_HOLE_SLICE)
xo = self.hole_diam / 2
rs = composite_from_pts(rc, [(-xo, 0, GR_HOLE_H), (xo, 0, GR_HOLE_H)])
rs = composite_from_pts(rs, self.hole_centres)
Expand Down
Loading