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
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
</p>

<p align="center">
<img
src="assets/img/cover.png"
style="max-width: 480px; width: 80%"
<img
src="assets/img/cover.png"
style="max-width: 480px; width: 80%"
alt="Rendered keycap model created with Capistry"
/>
</p>
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
)

Expand Down
Binary file modified examples/filleting/caps.stl
Binary file not shown.
16 changes: 12 additions & 4 deletions examples/filleting/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
40 changes: 21 additions & 19 deletions src/capistry/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
]
132 changes: 101 additions & 31 deletions src/capistry/fillet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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)",
Expand All @@ -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
Expand All @@ -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)",
Expand All @@ -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,
)