diff --git a/README.md b/README.md
index d17f35b..9a3bf8a 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,9 @@
-
@@ -161,16 +161,19 @@ cap = RectangularCap(taper=taper)
### Fillet Strategies
Choose how edges are rounded:
```python
-# Uniform filleting
-fillet_strat = FilletUniform()
+# Uniform
+strat = FilletUniform()
-# Sides-first filleting
-fillet_strat = FilletSidesFirst()
+# Front-to-back, then left-to-right
+strat = FilletDepthWidth()
-# Sides-Last filleting
-fillet_strat = FilletSidesFirst()
+# Left-to-right, then front-to-back
+strat = FilletWidthDepth()
-cap = RectangularCap(fillet_strategy=fillet_strat)
+# Mid-height edges, then top-perimeter
+strat = FilletMiddleTop()
+
+cap = RectangularCap(fillet_strategy=strat)
```
> [!WARNING]
@@ -214,8 +217,8 @@ cap = RectangularCap()
# Create a 4x4 panel of a keycap
panel = Panel(
- items=[PanelItem(cap, quantity=16)],
- sprue=SprueCylinder(),
+ items=[PanelItem(cap, quantity=16)],
+ sprue=SprueCylinder(),
cols=4
)
diff --git a/examples/filleting/caps.stl b/examples/filleting/caps.stl
index 751a084..c6c1e49 100644
Binary files a/examples/filleting/caps.stl and b/examples/filleting/caps.stl differ
diff --git a/examples/filleting/main.py b/examples/filleting/main.py
index 3984c7d..2a9f12d 100644
--- a/examples/filleting/main.py
+++ b/examples/filleting/main.py
@@ -38,20 +38,28 @@
roof=roof,
taper=taper,
stem=stem,
- fillet_strategy=FilletUniform(),
+ fillet_strategy=FilletUniform(), # Uniform fillet, every outside edge gets the same radius
)
+# Fillet left-to-right, then front-to-back
c2 = c1.clone()
-c2.fillet_strategy = FilletSidesFirst()
+c2.fillet_strategy = FilletWidthDepth()
c2.build()
c2.locate(c1.top_right * Pos(X=gap))
+# Fillet front-to-back, then left-to-right
c3 = c1.clone()
-c3.fillet_strategy = FilletSidesLast()
+c3.fillet_strategy = FilletDepthWidth()
c3.build()
c3.locate(c2.top_right * Pos(X=gap))
-caps = [c1.compound, c2.compound, c3.compound]
+# Fillet middle edges first, then top perimeter
+c4 = c1.clone()
+c4.fillet_strategy = FilletMiddleTop()
+c4.build()
+c4.locate(c3.top_right * Pos(X=gap))
+
+caps = [c1.compound, c2.compound, c3.compound, c4.compound]
show(*caps)
diff --git a/src/capistry/__init__.py b/src/capistry/__init__.py
index eac2423..5c381d6 100644
--- a/src/capistry/__init__.py
+++ b/src/capistry/__init__.py
@@ -1,15 +1,16 @@
"""
.. include:: ../../README.md
-"""
+""" # noqa: D200, D400
from .cap import Cap, RectangularCap, SkewedCap, SlantedCap, TrapezoidCap
from .compare import BaseComparer, Comparer
from .ergogen import Ergogen, ErgogenSchema
from .fillet import (
- FilletSidesFirst,
- FilletSidesLast,
+ FilletDepthWidth,
+ FilletMiddleTop,
FilletStrategy,
FilletUniform,
+ FilletWidthDepth,
fillet_safe,
)
from .logger import init_logger
@@ -20,29 +21,30 @@
from .taper import Taper
__all__ = [
+ "BaseComparer",
"Cap",
- "RectangularCap",
- "SkewedCap",
- "SlantedCap",
- "TrapezoidCap",
- "FilletSidesFirst",
- "FilletSidesLast",
+ "ChocStem",
+ "Comparer",
+ "Ergogen",
+ "ErgogenSchema",
+ "FilletDepthWidth",
+ "FilletMiddleTop",
"FilletStrategy",
"FilletUniform",
- "fillet_safe",
+ "FilletWidthDepth",
+ "MXStem",
"Panel",
"PanelItem",
- "Comparer",
- "BaseComparer",
- "ChocStem",
- "MXStem",
+ "RectangularCap",
+ "SkewedCap",
+ "SlantedCap",
+ "Sprue",
+ "SprueCylinder",
+ "SpruePolygon",
"Stem",
"Surface",
"Taper",
+ "TrapezoidCap",
+ "fillet_safe",
"init_logger",
- "Sprue",
- "SprueCylinder",
- "SpruePolygon",
- "Ergogen",
- "ErgogenSchema",
]
diff --git a/src/capistry/fillet.py b/src/capistry/fillet.py
index 7f18973..a9dac8a 100644
--- a/src/capistry/fillet.py
+++ b/src/capistry/fillet.py
@@ -4,17 +4,19 @@
This module provides various fillet strategies for applying rounded edges to
keyboard caps including MXStem, ChocStem, and other Cap-type classes. The strategies
allow for customizable filleting of outer edges, inner edges, and skirt areas.
-e
+
Classes
-------
FilletStrategy : ABC
Abstract base class for all fillet strategies.
FilletUniform : FilletStrategy
Applies uniform outer fillets to all edges.
-FilletSidesFirst : FilletStrategy
- Applies differentiated fillets to sides before other edges.
-FilletSidesLast : FilletStrategy
- Applies differentiated fillets to sides after other edges.
+FilletWidthDepth : FilletStrategy
+ Applies differentiated fillets left-to-right, then front-to-back.
+FilletDepthWidth : FilletStrategy
+ Applies differentiated fillets front-to-back, then left-to-right.
+FilletMiddleTop : FilletStrategy
+ Applies differentiated fillets to mid-height edges, then top perimeter.
Functions
---------
@@ -33,7 +35,18 @@
from dataclasses import dataclass, fields
from typing import Self
-from build123d import Axis, BuildPart, ChamferFilletType, Curve, Part, Select, Sketch, fillet
+from build123d import (
+ Axis,
+ BuildPart,
+ ChamferFilletType,
+ Curve,
+ Face,
+ Part,
+ Select,
+ ShapeList,
+ Sketch,
+ fillet,
+)
from capistry.compare import Comparable, Metric, MetricGroup, MetricLayout
@@ -89,10 +102,10 @@ def fillet_safe(
The Build123d objects (edges, faces) to fillet.
radius : float
The fillet radius in millimeters.
- threshold : float, default 1e-6
+ threshold : float, default=1e-6
The minimum radius required to attempt a fillet operation.
Values less than or equal to this will skip the operation.
- err : bool, default True
+ err : bool, default=True
Whether to raise FilletError on failure. If False, returns None on failure.
Returns
@@ -136,7 +149,7 @@ class FilletStrategy(Comparable, ABC):
Abstract base class for `capistry.Cap` fillet strategies.
Defines the interface for applying various types of fillets to caps
- such as `capistry.TrapezoidCap`, `capistry.RectangularCap`, and any other `capistry.Cap`
+ such as `capistry.TrapezoidCap`, `capistry.RectangularCap`, or any other `capistry.Cap`
subclasses. Provides common parameters and methods for inner and skirt filleting
while leaving outer fillet implementation to concrete subclasses.
@@ -304,16 +317,16 @@ def apply_outer(self, p: BuildPart):
@dataclass
-class FilletSidesFirst(FilletStrategy):
+class FilletWidthDepth(FilletStrategy):
"""
- Directional fillet strategy applying side fillets before other edges.
+ Directional fillet strategy applying width edges (left/right) before depth edges (front/back).
Parameters
----------
front : float, default=3.0
- Radius in mm for front edge fillets (positive Y direction).
+ Radius in mm for front edge fillets (negative Y direction).
back : float, default=2.0
- Radius in mm for back edge fillets (negative Y direction).
+ Radius in mm for back edge fillets (positive Y direction).
left : float, default=1.0
Radius in mm for left side fillets (negative X direction).
right : float, default=1.0
@@ -331,16 +344,13 @@ class FilletSidesFirst(FilletStrategy):
def apply_outer(self, p: BuildPart):
"""
- Apply directional outer fillets with sides processed first.
-
- Applies different fillet radii to each side of the keycap, with
- side edges processed last.
+ Apply outer fillet radii with (left/right) processed first.
Parameters
----------
p : BuildPart
- The BuildPart representing a Cap instance (MXStem, choc, etc.) to
- which directional outer fillets should be applied.
+ The BuildPart representing a `capistry.Cap` instance to
+ which outer fillets should be applied.
"""
logger.debug(
"Applying outer fillets (sides-first)",
@@ -359,20 +369,20 @@ def apply_outer(self, p: BuildPart):
@dataclass
-class FilletSidesLast(FilletStrategy):
+class FilletDepthWidth(FilletStrategy):
"""
- Directional fillet strategy applying side fillets after other edges.
+ Directional fillet strategy applying depth edges (front/back) before width edges (left/right).
Parameters
----------
front : float, default=2.0
- Radius in mm for front edge fillets (minimum Y direction).
+ Radius in mm for front edge fillets (negative Y direction).
back : float, default=1.0
- Radius in mm for back edge fillets (maximum Y direction).
+ Radius in mm for back edge fillets (positive Y direction).
left : float, default=3.0
- Radius in mm for left side fillets (minimum X direction).
+ Radius in mm for left side fillets (negative X direction).
right : float, default=3.0
- Radius in mm for right side fillets (maximum X direction).
+ Radius in mm for right side fillets (positive X direction).
skirt : float, default=0.25
Inherited from FilletStrategy. Radius for bottom face edge fillets.
inner : float, default 1.0
@@ -386,16 +396,13 @@ class FilletSidesLast(FilletStrategy):
def apply_outer(self, p: BuildPart):
"""
- Apply directional outer fillets with sides processed last.
-
- Applies different fillet radii to each side of the keycap, with
- side edges processed last.
+ Apply outer fillet radii with (left/right) processed last.
Parameters
----------
p : BuildPart
- The BuildPart representing a Cap instance (MXStem, choc, etc.) to
- which directional outer fillets should be applied.
+ The BuildPart representing a `capistry.Cap` instance to
+ which outer fillets should be applied.
"""
logger.debug(
"Applying outer fillets (sides-last)",
@@ -411,3 +418,66 @@ def apply_outer(self, p: BuildPart):
fillet_safe(p.faces().sort_by(Axis.Z)[-1].edges().sort_by(Axis.X)[-1], self.right)
fillet_safe(p.faces().sort_by(Axis.Y)[0].edges().sort_by(Axis.Z)[1:], self.front)
fillet_safe(p.faces().sort_by(Axis.Y)[-1].edges().sort_by(Axis.Z)[1:], self.back)
+
+
+@dataclass
+class FilletMiddleTop(FilletStrategy):
+ """
+ Fillet strategy that applies middle-height edges first, then top face edges.
+
+ Parameters
+ ----------
+ top : float, default=0.75
+ Radius for the topmost edge fillets (upper Z direction).
+ middle : float, default=2.5
+ Radius for edges located in the mid-section of the part.
+ skirt : float, default=0.25
+ Inherited from FilletStrategy. Radius for bottom face edge fillets.
+ inner : float, default=1.0
+ Inherited from FilletStrategy. Radius for internal Z-axis edge fillets.
+ """
+
+ top: float = 0.75
+ middle: float = 2.5
+
+ def apply_outer(self, p: BuildPart):
+ """
+ Apply mid-section and top edge fillets to the outer geometry.
+
+ First applies fillets to mid-height vertical or slanted edges,
+ followed by fillets on the uppermost perimeter.
+
+ Parameters
+ ----------
+ p : BuildPart
+ The BuildPart representing a Cap instance (e.g., MXStem or Choc).
+ """
+ logger.debug(
+ "Applying outer fillets (top-to-middle)",
+ extra={"top": self.top, "middle": self.middle},
+ )
+
+ def faces() -> ShapeList[Face]:
+ # Sort faces by height (Z) and prioritize side faces
+ # (i.e., those with normals not closely aligned with Z).
+ return (
+ p.faces()
+ .sort_by(Axis.Z, reverse=True)
+ .sort_by(lambda f: abs(f.normal_at().dot(Axis.Z.direction)))
+ )
+
+ # Fillet mid-height edges on side faces:
+ # - Exclude top/bottom Z-aligned faces (last two in list).
+ # - Use `% 0.5` to find the edge tangent at midpoint.
+ # - Keep edges with significant vertical (Z) tangent component.
+ fillet_safe(
+ faces()[:-2].edges().filter_by(lambda e: abs((e % 0.5).Z) > 0.5), # noqa: PLR2004
+ self.middle,
+ )
+
+ # Fillet the topmost usable face's edges:
+ # - From last two faces, pick the highest one by Z position.
+ fillet_safe(
+ faces()[-2:].sort_by(Axis.Z)[-1].edges(),
+ self.top,
+ )