diff --git a/src/build123d/__init__.py b/src/build123d/__init__.py index a397251..d0bd94a 100644 --- a/src/build123d/__init__.py +++ b/src/build123d/__init__.py @@ -112,12 +112,14 @@ __all__ = [ "Matrix", "Solid", "Shell", + "Part", "Plane", "Compound", "Location", "Joint", "RigidJoint", "RevoluteJoint", + "Sketch", "LinearJoint", "CylindricalJoint", "BallJoint", diff --git a/src/build123d/algebra.py b/src/build123d/algebra.py index 4d98699..d1a4649 100644 --- a/src/build123d/algebra.py +++ b/src/build123d/algebra.py @@ -1,35 +1,43 @@ import copy from typing import Any, List, Union from build123d.build_enums import * -from build123d.topology import Location, Wire, Plane, Vertex, Vector, Compound +from build123d.topology import ( + Location, + Wire, + Plane, + Vertex, + Vector, + Compound, + AlgebraMixin, +) -class SkipClean: - """Skip clean context for use in operator driven code where clean=False wouldn't work""" +# class SkipClean: +# """Skip clean context for use in operator driven code where clean=False wouldn't work""" - clean = True +# clean = True - def __enter__(self): - SkipClean.clean = False +# def __enter__(self): +# SkipClean.clean = False - def __exit__(self, exception_type, exception_value, traceback): - SkipClean.clean = True +# def __exit__(self, exception_type, exception_value, traceback): +# SkipClean.clean = True -def listify(arg: Any) -> List: - if isinstance(arg, (tuple, list)): - return list(arg) - else: - return [arg] +# def listify(arg: Any) -> List: +# if isinstance(arg, (tuple, list)): +# return list(arg) +# else: +# return [arg] -def is_algcompound(object): - return ( - hasattr(object, "wrapped") - and hasattr(object, "_dim") - and hasattr(object, "_is_alg") - and object._is_alg - ) +# def is_algcompound(object): +# return ( +# hasattr(object, "wrapped") +# and hasattr(object, "_dim") +# and hasattr(object, "_is_alg") +# and object._is_alg +# ) class Pos(Location): @@ -52,91 +60,91 @@ class AlgLine(Compound): self._dim = 1 -class AlgebraMixin: - def _place(self, mode: Mode, *objs: Any): - # TODO error handling for non algcompound objects +# class AlgebraMixin: +# def _place(self, mode: Mode, *objs: Any): +# # TODO error handling for non algcompound objects - if not all([is_algcompound(o) for o in objs]): - raise RuntimeError( - "Non-algebraic operand(s) found in algebraic function. Are you in a context?" - ) +# if not all([is_algcompound(o) for o in objs]): +# raise RuntimeError( +# "Non-algebraic operand(s) found in algebraic function. Are you in a context?" +# ) - if not (objs[0]._dim == 0 or self._dim == 0 or self._dim == objs[0]._dim): - raise RuntimeError( - f"Cannot combine objects of different dimensionality: {self._dim} and {objs[0]._dim}" - ) +# if not (objs[0]._dim == 0 or self._dim == 0 or self._dim == objs[0]._dim): +# raise RuntimeError( +# f"Cannot combine objects of different dimensionality: {self._dim} and {objs[0]._dim}" +# ) - if self._dim == 0: # Cover addition of empty BuildPart with another object - if mode == Mode.ADD: - if len(objs) == 1: - compound = copy.deepcopy(objs[0]) - else: - compound = copy.deepcopy(objs.pop()).fuse(*objs) - else: - raise RuntimeError("Can only add to an empty BuildPart object") - elif objs[0]._dim == 0: # Cover operation with empty BuildPart object - compound = self - else: - if mode == Mode.ADD: - compound = self.fuse(*objs) +# if self._dim == 0: # Cover addition of empty BuildPart with another object +# if mode == Mode.ADD: +# if len(objs) == 1: +# compound = copy.deepcopy(objs[0]) +# else: +# compound = copy.deepcopy(objs.pop()).fuse(*objs) +# else: +# raise RuntimeError("Can only add to an empty BuildPart object") +# elif objs[0]._dim == 0: # Cover operation with empty BuildPart object +# compound = self +# else: +# if mode == Mode.ADD: +# compound = self.fuse(*objs) - elif self._dim == 1: - raise RuntimeError("Lines can only be added") +# elif self._dim == 1: +# raise RuntimeError("Lines can only be added") - else: - if mode == Mode.SUBTRACT: - compound = self.cut(*objs) - elif mode == Mode.INTERSECT: - compound = self.intersect(*objs) +# else: +# if mode == Mode.SUBTRACT: +# compound = self.cut(*objs) +# elif mode == Mode.INTERSECT: +# compound = self.intersect(*objs) - if SkipClean.clean: - compound = compound.clean() +# if SkipClean.clean: +# compound = compound.clean() - compound = self._wrappper_cls(compound.wrapped) +# compound = self._wrappper_cls(compound.wrapped) - return compound +# return compound - # TODO: How to use typing here - def __add__(self, other: Union[Any, List[Any]]): - return self._place(Mode.ADD, *listify(other)) +# # TODO: How to use typing here +# def __add__(self, other: Union[Any, List[Any]]): +# return self._place(Mode.ADD, *listify(other)) - # TODO: How to use typing here - def __sub__(self, other: Union[Any, List[Any]]): - return self._place(Mode.SUBTRACT, *listify(other)) +# # TODO: How to use typing here +# def __sub__(self, other: Union[Any, List[Any]]): +# return self._place(Mode.SUBTRACT, *listify(other)) - # TODO: How to use typing here - def __and__(self, other: Union[Any, List[Any]]): - return self._place(Mode.INTERSECT, *listify(other)) +# # TODO: How to use typing here +# def __and__(self, other: Union[Any, List[Any]]): +# return self._place(Mode.INTERSECT, *listify(other)) - def __mul__(self, loc: Location): - if self._dim == 3: - return copy.copy(self).move(loc) - else: - return self.moved(loc) +# def __mul__(self, loc: Location): +# if self._dim == 3: +# return copy.copy(self).move(loc) +# else: +# return self.moved(loc) - def __matmul__(self, obj: Union[float, Location, Plane]): - if isinstance(obj, (int, float)): - if self._dim == 1: - return Wire.make_wire(self.edges()).position_at(obj) - else: - raise TypeError("Only lines can access positions") +# def __matmul__(self, obj: Union[float, Location, Plane]): +# if isinstance(obj, (int, float)): +# if self._dim == 1: +# return Wire.make_wire(self.edges()).position_at(obj) +# else: +# raise TypeError("Only lines can access positions") - elif isinstance(obj, Location): - loc = obj +# elif isinstance(obj, Location): +# loc = obj - elif isinstance(obj, Plane): - loc = obj.to_location() +# elif isinstance(obj, Plane): +# loc = obj.to_location() - else: - raise ValueError(f"Cannot multiply with {obj}") +# else: +# raise ValueError(f"Cannot multiply with {obj}") - if self._dim == 3: - return copy.copy(self).locate(loc) - else: - return self.located(loc) +# if self._dim == 3: +# return copy.copy(self).locate(loc) +# else: +# return self.located(loc) - def __mod__(self, position): - if self._dim == 1: - return Wire.make_wire(self.edges()).tangent_at(position) - else: - raise TypeError(f"unsupported operand type(s)") +# def __mod__(self, position): +# if self._dim == 1: +# return Wire.make_wire(self.edges()).tangent_at(position) +# else: +# raise TypeError(f"unsupported operand type(s)") diff --git a/src/build123d/build_part.py b/src/build123d/build_part.py index 7f64a99..9520a97 100644 --- a/src/build123d/build_part.py +++ b/src/build123d/build_part.py @@ -43,14 +43,7 @@ from build123d.geometry import ( Vector, VectorLike, ) -from build123d.topology import ( - Compound, - Edge, - Face, - Shell, - Solid, - Wire, -) +from build123d.topology import Compound, Edge, Face, Shell, Solid, Wire, Part from build123d.build_common import ( Builder, @@ -60,7 +53,7 @@ from build123d.build_common import ( validate_inputs, ) -from build123d.algebra import AlgebraMixin +# from build123d.algebra import AlgebraMixin class BuildPart(Builder): @@ -220,6 +213,11 @@ class BuildPart(Builder): len(new_solids), mode, ) + if self.part: + if not isinstance(self.part, Compound): + self.part = Part(Compound.make_compound(self.part.solids()).wrapped) + else: + self.part = Part(self.part.wrapped) post_vertices = set() if self.part is None else set(self.part.vertices()) post_edges = set() if self.part is None else set(self.part.edges()) @@ -251,12 +249,12 @@ class BuildPart(Builder): return result -class Part(Compound, AlgebraMixin): - def __init__(self, wrapped, is_alg=True): - super().__init__(wrapped) - self._is_alg = is_alg - self._dim = 3 - self._wrappper_cls = Part +# class Part(Compound, AlgebraMixin): +# def __init__(self, wrapped, is_alg=True): +# super().__init__(wrapped) +# self._is_alg = is_alg +# self._dim = 3 +# self._wrappper_cls = Part class BasePartObject(Part): diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 172ad9c..f83bba0 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -252,6 +252,7 @@ from build123d.build_enums import ( FrameMethod, GeomType, Kind, + Mode, PositionMode, SortBy, Transition, @@ -1096,6 +1097,7 @@ class Shape(NodeMixin): joints: dict[str, Joint] = None, parent: Compound = None, children: list[Shape] = None, + is_alg: bool = False, ): self.wrapped = downcast(obj) if obj else None self.for_construction = False @@ -1115,6 +1117,21 @@ class Shape(NodeMixin): # parent must be set following children as post install accesses children self.parent = parent + if isinstance(self, Part): + self._is_alg: bool = is_alg + self._dim: int = 3 + self._wrappper_cls: Shape = Part + + if isinstance(self, Sketch): + self._is_alg: bool = is_alg + self._dim: int = 2 + self._wrappper_cls: Shape = Sketch + + if isinstance(self, LineLine): + self._is_alg: bool = is_alg + self._dim: int = 1 + self._wrappper_cls: Shape = LineLine + @property def location(self) -> Location: """Get this Shape's Location""" @@ -3232,6 +3249,108 @@ class Compound(Shape, Mixin3D): return results +class AlgebraMixin: + def _place(self, mode: Mode, *objs: Any): + # TODO error handling for non algcompound objects + + if not all([is_algcompound(o) for o in objs]): + raise RuntimeError( + "Non-algebraic operand(s) found in algebraic function. Are you in a context?" + ) + + if not (objs[0]._dim == 0 or self._dim == 0 or self._dim == objs[0]._dim): + raise RuntimeError( + f"Cannot combine objects of different dimensionality: {self._dim} and {objs[0]._dim}" + ) + + if self._dim == 0: # Cover addition of empty BuildPart with another object + if mode == Mode.ADD: + if len(objs) == 1: + compound = copy.deepcopy(objs[0]) + else: + compound = copy.deepcopy(objs.pop()).fuse(*objs) + else: + raise RuntimeError("Can only add to an empty BuildPart object") + elif objs[0]._dim == 0: # Cover operation with empty BuildPart object + compound = self + else: + if mode == Mode.ADD: + compound = self.fuse(*objs) + + elif self._dim == 1: + raise RuntimeError("Lines can only be added") + + else: + if mode == Mode.SUBTRACT: + compound = self.cut(*objs) + elif mode == Mode.INTERSECT: + compound = self.intersect(*objs) + + if SkipClean.clean: + compound = compound.clean() + + compound = self._wrappper_cls(compound.wrapped) + + return compound + + # TODO: How to use typing here + def __add__(self, other: Union[Any, List[Any]]): + return self._place(Mode.ADD, *listify(other)) + + # TODO: How to use typing here + def __sub__(self, other: Union[Any, List[Any]]): + return self._place(Mode.SUBTRACT, *listify(other)) + + # TODO: How to use typing here + def __and__(self, other: Union[Any, List[Any]]): + return self._place(Mode.INTERSECT, *listify(other)) + + def __mul__(self, loc: Location): + if self._dim == 3: + return copy.copy(self).move(loc) + else: + return self.moved(loc) + + def __matmul__(self, obj: Union[float, Location, Plane]): + if isinstance(obj, (int, float)): + if self._dim == 1: + return Wire.make_wire(self.edges()).position_at(obj) + else: + raise TypeError("Only lines can access positions") + + elif isinstance(obj, Location): + loc = obj + + elif isinstance(obj, Plane): + loc = obj.to_location() + + else: + raise ValueError(f"Cannot multiply with {obj}") + + if self._dim == 3: + return copy.copy(self).locate(loc) + else: + return self.located(loc) + + def __mod__(self, position): + if self._dim == 1: + return Wire.make_wire(self.edges()).tangent_at(position) + else: + raise TypeError(f"unsupported operand type(s)") + + +class Part(Compound, AlgebraMixin): + pass + + +class Sketch(Compound, AlgebraMixin): + pass + + +class LineLine(Compound, AlgebraMixin): + pass + + class Edge(Shape, Mixin1D): """A trimmed curve that represents the border of a face""" @@ -7065,3 +7184,31 @@ def sort_wires_by_build_order(wire_list: list[Wire]) -> list[list[Wire]]: def polar(length: float, angle: float) -> tuple[float, float]: """Convert polar coordinates into cartesian coordinates""" return (length * cos(radians(angle)), length * sin(radians(angle))) + + +def listify(arg: Any) -> List: + if isinstance(arg, (tuple, list)): + return list(arg) + else: + return [arg] + + +def is_algcompound(object): + return ( + hasattr(object, "wrapped") + and hasattr(object, "_dim") + and hasattr(object, "_is_alg") + and object._is_alg + ) + + +class SkipClean: + """Skip clean context for use in operator driven code where clean=False wouldn't work""" + + clean = True + + def __enter__(self): + SkipClean.clean = False + + def __exit__(self, exception_type, exception_value, traceback): + SkipClean.clean = True