diff --git a/README.md b/README.md index d17f35b..9a3bf8a 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@

- Rendered keycap model created with Capistry

@@ -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, + )