"""Utiity module."""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum, auto
from build123d import (
Align,
Axis,
BasePartObject,
BaseSketchObject,
BuildLine,
BuildPart,
BuildSketch,
Face,
Kind,
Location,
Locations,
Mode,
Part,
Plane,
Polyline,
Rectangle,
RectangleRounded,
RotationLike,
Sketch,
add, # pyright: ignore[reportUnknownVariableType]
extrude,
fillet,
make_face,
offset,
sweep,
)
from .constants import gridfinity_standard
[docs]
class UnsuportedEnumValueError(Exception):
"""Raised when a unsuported enum value is handled."""
def __init__(self, enum_var: Enum) -> None:
"""Construct Enum exception.
Args:
enum_var (Enum): Enum value
"""
super().__init__(f"Unsuported enum value: {enum_var}")
[docs]
class ObjectCreate(ABC):
"""Interface for object forcing to implement create_obj."""
[docs]
@abstractmethod
def create_obj(
self,
rotation: RotationLike = (0, 0, 0),
align: Align | tuple[Align, Align, Align] | None = None,
mode: Mode = Mode.ADD,
) -> BasePartObject:
"""Create the build123d 3d object.
Args:
rotation (RotationLike, optional): Angels to rotate around axes. Defaults to (0, 0, 0).
align (Union[Align, tuple[Align, Align, Align]], optional): Align min center of max of
object. Defaults to None.
mode (Mode, optional): Combination mode. Defaults to Mode.ADD.
Raises:
NotImplementedError: Child class does not have an implementation
Returns:
BasePartObject: The build123d 3d object
"""
raise NotImplementedError # pragma: no cover
[docs]
class Direction(Enum):
"""Direction Enum.
Direction can be converted to a tuple.
"""
TOP = auto()
BOT = auto()
RIGHT = auto()
LEFT = auto()
BACK = auto()
FRONT = auto()
[docs]
@staticmethod
def to_tuple(direction: Direction) -> tuple[int, int, int]:
"""Convert Direction to tuple.
Args:
direction (Direction): Direction to convert.
Raises:
ValueError: Unkonw Direction.
Returns:
Tuple[int, int, int]: Output tupple.
"""
match direction:
case Direction.TOP:
return (0, 0, 1)
case Direction.BOT:
return (0, 0, -1)
case Direction.RIGHT:
return (1, 0, 0)
case Direction.LEFT:
return (-1, 0, 0)
case Direction.BACK:
return (0, 1, 0)
case Direction.FRONT:
return (0, -1, 0)
[docs]
class Attach(Enum):
"""Attach.
Enum for indicating a direction when attaching objects
"""
TOP = auto()
BOTTOM = auto()
LEFT = auto()
RIGHT = auto()
BACK = auto()
FRONT = auto()
[docs]
class Utils: # pylint: disable=too-few-public-methods
"""Utils.
Class wrapping utility functions
"""
[docs]
@staticmethod
def attach(
context: BuildPart,
part: Part,
attach: Attach,
offset_value: float = 0,
) -> None:
"""Attach.
Attaches other object acording to "attach".
Args:
context (BuildPart): context were attach should be executed.
part (Part): the part to be attached
attach (Attach): Direction to attach
offset_value (float, optional): offset. Defaults to 0.
Raises:
UnsuportedEnumValueError: Unsuported Enum value
"""
context_part = context.part
if not isinstance(context_part, Part): # pragma: no cover
msg = "Context has no part"
raise TypeError(msg)
location: tuple[float, float, float] = (0, 0, 0)
match attach:
case Attach.TOP:
location = (
0,
0,
context_part.bounding_box().max.Z
+ -1 * part.bounding_box().min.Z
+ offset_value,
)
case Attach.BOTTOM:
location = (
0,
0,
context_part.bounding_box().min.Z
+ -1 * part.bounding_box().max.Z
- offset_value,
)
case Attach.LEFT:
location = (
context_part.bounding_box().min.X
+ -1 * part.bounding_box().max.X
- offset_value,
0,
0,
)
case Attach.RIGHT:
location = (
context_part.bounding_box().max.X
+ -1 * part.bounding_box().min.X
+ offset_value,
0,
0,
)
case Attach.FRONT:
location = (
0,
context_part.bounding_box().min.Y
+ -1 * part.bounding_box().max.Y
- offset_value,
0,
)
case Attach.BACK:
location = (
0,
context_part.bounding_box().max.Y
+ -1 * part.bounding_box().min.Y
+ offset_value,
0,
)
with Locations(location):
_ = add(part)
[docs]
@staticmethod
def get_face_by_direction(context: BuildPart, direction: Direction) -> Face:
"""Get face by direction.
Args:
context (BuildPart): context were attach should be executed.
direction (Direction): Direction of face
Returns:
Face: face
"""
return context.faces().sort_by(Axis((0, 0, 0), Direction.to_tuple(direction)))[-1]
[docs]
@staticmethod
def get_subclasses(class_name: type) -> list[type]:
"""Get subclasses of a base class recursively.
Args:
class_name (Any): class type to get subcalsses from
Returns:
Any: list of child class types
"""
classes: list[type] = []
for subclass in class_name.__subclasses__():
classes.append(subclass)
classes += Utils.get_subclasses(subclass)
return classes
[docs]
@staticmethod
def locate_grid(grid: list[list[bool]], width: float, length: float) -> list[Location]:
"""Generate locations for a grid with given spacing in x and y directions.
Args:
grid (list[list[bool]]): grid
width (float): spacing of the grid in the x-direction.
length (float): spacing of the grid in the y-direction.
Returns:
list[Location]: the locations for the grid.
"""
locations: list[Location] = []
for row_nr, row_value in enumerate(grid):
for column_nr, column_value in enumerate(row_value):
if column_value:
locations.append(
Location((width * (column_nr + 1), length * -(row_nr + 1))),
)
return locations
[docs]
@staticmethod
def place_sketch_by_grid(
obj: Sketch,
grid: list[list[bool]],
width: float | None = None,
length: float | None = None,
rotation: float = 0,
align: Align | tuple[Align, Align] = Align.CENTER,
mode: Mode = Mode.ADD,
) -> BaseSketchObject:
"""Place multiple instances of a sketch according to a grid.
Args:
obj (BaseSketchObject): sketch to be copied
grid (list[list[bool]]): grid
width (float | None): spacing of the grid in the x-direction. Defaults to None.
length (float | None): spacing of the grid in the y-direction. Defaults to None.
rotation (RotationLike, optional): Angels to rotate around axes. Defaults to (0, 0, 0).
align (Union[Align, tuple[Align, Align, Align]], optional): Align min center of max of
object. Defaults to Align.CENTER.
mode (Mode, optional): Combination mode. Defaults to Mode.ADD.
Returns:
BaseSketchObject: gridlike sketch
"""
bbox = obj.bounding_box()
if width is None:
width = bbox.size.X
if length is None:
length = bbox.size.Y
locations = Utils.locate_grid(grid, width, length)
with BuildSketch() as sketch, Locations(locations):
_ = add(obj)
return BaseSketchObject(sketch.sketch, rotation, align, mode)
[docs]
@staticmethod
def place_by_grid(
obj: BasePartObject,
grid: list[list[bool]],
width: float | None = None,
length: float | None = None,
rotation: RotationLike = (0, 0, 0),
align: Align | tuple[Align, Align, Align] = Align.CENTER,
mode: Mode = Mode.ADD,
) -> BasePartObject:
"""Place multiple instances of object according to grid.
Args:
obj (BasePartObject): object to be copied
grid (list[list[bool]]): grid
width (float | None): spacing of the grid in the x-direction. Defaults to None.
length (float | None): spacing of the grid in the y-direction. Defaults to None.
rotation (RotationLike, optional): Angels to rotate around axes. Defaults to (0, 0, 0).
align (Union[Align, tuple[Align, Align, Align]], optional): Align min center of max of
object. Defaults to Align.CENTER.
mode (Mode, optional): Combination mode. Defaults to Mode.ADD.
Raises:
ValueError: grid does not reasemble locations
Returns:
BasePartObject: gridlike object
"""
if not width or not length:
bbox = obj.bounding_box()
if width is None:
width = bbox.size.X
if length is None:
length = bbox.size.Y
locations = Utils.locate_grid(grid, width, length)
if not locations:
msg = f"grid {grid} does not reasemble locations"
raise ValueError(msg)
with BuildPart() as part, Locations(locations):
_ = add(obj)
if not part.part: # pragma: no cover
msg = "Part is empty"
raise RuntimeError(msg)
return BasePartObject(part.part, rotation, align, mode)
[docs]
@staticmethod
def create_profile_block(
profile_type: StackProfile.ProfileType,
offset_value: float = 0,
rotation: RotationLike = (0, 0, 0),
align: Align | tuple[Align, Align, Align] | None = None,
mode: Mode = Mode.ADD,
) -> BasePartObject:
"""Create block with stacing profile.
Args:
profile_type (StackProfile.ProfileType): Profile type
offset_value (float, optional): Offset of profile sweep. Defaults to 0.
rotation (RotationLike, optional): Angels to rotate around axes. Defaults to (0, 0, 0).
align (Union[Align, tuple[Align, Align, Align]], optional): Align min center of max of
object. Defaults to None.
mode (Mode, optional): Combination mode. Defaults to Mode.ADD.
Returns:
BasePartObject: _description_
"""
with BuildPart() as part:
with (
BuildSketch(Plane.XZ) as profile,
Locations(
(
gridfinity_standard.grid.size / 2 - offset_value,
0,
),
),
):
_ = StackProfile(profile_type, align=(Align.MAX, Align.MIN))
if offset_value:
_ = offset(
amount=offset_value,
kind=Kind.INTERSECTION,
)
with BuildSketch() as rect:
if profile_type == StackProfile.ProfileType.BIN:
e = gridfinity_standard.grid.tollerance
else:
e = 0.0
_ = RectangleRounded(
gridfinity_standard.grid.size - e,
gridfinity_standard.grid.size - e,
gridfinity_standard.grid.radius - e * 0.5,
)
_ = extrude(to_extrude=rect.face(), amount=profile.sketch.bounding_box().max.Z)
path = part.wires().sort_by(Axis.Z)[-1]
_ = sweep(sections=profile.sketch, path=path, mode=Mode.SUBTRACT)
if not part.part: # pragma: no cover
msg = "Part is empty"
raise RuntimeError(msg)
return BasePartObject(part.part, rotation, align, mode)
[docs]
class StackProfile(BaseSketchObject):
"""Gridfinity stacking profile."""
[docs]
class ProfileType(Enum):
"""Profile Type."""
BIN = auto()
PLATE = auto()
def __init__(
self,
stack_type: ProfileType,
rotation: float = 0,
align: Align | tuple[Align, Align] | None = None,
mode: Mode = Mode.ADD,
):
"""StackProfile.
Create a profile of the gridfinity stacking system. Usualy used in the sweep function.
Args:
stack_type (ProfileType): Type of stacking lip (Bin vs Plate).
rotation (float, optional): angles to rotate objects. Defaults to 0.
align (Union[Align, tuple[Align, Align, Align]], optional): Align min center of max of
object. Defaults to None.
mode (Mode, optional): Combination mode. Defaults to Mode.ADD.
"""
match stack_type:
case StackProfile.ProfileType.BIN:
height_3 = gridfinity_standard.stacking_lip.height_3_bin
case StackProfile.ProfileType.PLATE:
height_3 = gridfinity_standard.stacking_lip.height_3_base_plate
with BuildSketch() as profile:
with BuildLine():
_ = Polyline(
(0, 0),
(
gridfinity_standard.stacking_lip.height_1,
gridfinity_standard.stacking_lip.height_1,
),
(
gridfinity_standard.stacking_lip.height_1,
gridfinity_standard.stacking_lip.height_1
+ gridfinity_standard.stacking_lip.height_2,
),
(
gridfinity_standard.stacking_lip.height_1 + height_3,
gridfinity_standard.stacking_lip.height_1
+ gridfinity_standard.stacking_lip.height_2
+ height_3,
),
(
gridfinity_standard.stacking_lip.height_1 + height_3,
0,
),
close=True,
)
_ = make_face()
super().__init__(profile.face(), rotation, align, mode)