From 2bbf02b8c6badc13a78836ae2211ec9a75839fa5 Mon Sep 17 00:00:00 2001 From: Roger Maitland Date: Sun, 19 Mar 2023 14:29:54 -0400 Subject: [PATCH] Moving ahead with algebra for lines --- src/build123d/build_common.py | 21 ++++++---- src/build123d/build_line.py | 77 +++++++++++++++++++++++------------ src/build123d/build_part.py | 4 +- src/build123d/build_sketch.py | 4 +- src/build123d/geometry.py | 2 +- src/build123d/topology.py | 12 ++++-- tests/test_direct_api.py | 2 +- 7 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/build123d/build_common.py b/src/build123d/build_common.py index a52ff90..3eda36a 100644 --- a/src/build123d/build_common.py +++ b/src/build123d/build_common.py @@ -738,16 +738,19 @@ class WorkplaneList: - 1 point -> Vector - >1 points -> list[Vector] """ - points_per_workplane = [] - workplane = WorkplaneList._get_context().workplanes[0] - localized_pts = [ - workplane.from_local_coords(pt) if isinstance(pt, tuple) else pt - for pt in points - ] - if len(localized_pts) == 1: - points_per_workplane.append(localized_pts[0]) + if WorkplaneList._get_context() is None: + points_per_workplane = [Vector(p) for p in points] else: - points_per_workplane.extend(localized_pts) + points_per_workplane = [] + workplane = WorkplaneList._get_context().workplanes[0] + localized_pts = [ + workplane.from_local_coords(pt) if isinstance(pt, tuple) else pt + for pt in points + ] + if len(localized_pts) == 1: + points_per_workplane.append(localized_pts[0]) + else: + points_per_workplane.extend(localized_pts) if len(points_per_workplane) == 1: result = points_per_workplane[0] diff --git a/src/build123d/build_line.py b/src/build123d/build_line.py index 0688efd..08bd7f0 100644 --- a/src/build123d/build_line.py +++ b/src/build123d/build_line.py @@ -40,6 +40,7 @@ from build123d.geometry import ( ) from build123d.topology import ( Compound, + Curve, Edge, Face, ShapeList, @@ -94,7 +95,7 @@ class BuildLine(Builder): ): self.initial_plane = workplane self.mode = mode - self.line: Compound = None + self.line: Curve = None super().__init__(workplane, mode=mode) def __exit__(self, exception_type, exception_value, traceback): @@ -183,25 +184,34 @@ class BuildLine(Builder): if self.line: self.line = self.line.fuse(*new_edges) else: - self.line = Compound.make_compound(new_edges) + self.line = Curve(Compound.make_compound(new_edges).wrapped) + elif mode == Mode.SUBTRACT: + if self.line is None: + raise RuntimeError("No line to subtract from") + self.line = self.line.cut(*new_edges) + elif mode == Mode.INTERSECT: + if self.line is None: + raise RuntimeError("No line to intersect with") + self.line = self.line.intersect(*new_edges) elif mode == Mode.REPLACE: - self.line = Compound.make_compound(new_edges) + self.line = Curve(Compound.make_compound(new_edges).wrapped) self.last_edges = ShapeList(new_edges) self.last_vertices = ShapeList( set(v for e in self.last_edges for v in e.vertices()) ) @classmethod - def _get_context(cls, caller=None) -> "BuildLine": + def _get_context(cls, caller=None) -> BuildLine: """Return the instance of the current builder""" result = cls._current.get(None) - if caller is not None and result is None: - if hasattr(caller, "_applies_to"): - raise RuntimeError( - f"No valid context found, use one of {caller._applies_to}" - ) - raise RuntimeError("No valid context found") + # print(f"{result=}, {caller=}") + # if caller is not None and result is None: + # if hasattr(caller, "_applies_to"): + # raise RuntimeError( + # f"No valid context found, use one of {caller._applies_to}" + # ) + # raise RuntimeError("No valid context found") logger.info( "Context requested by %s", @@ -238,7 +248,8 @@ class BaseLineObject(Wire): ): context: BuildLine = BuildLine._get_context(self) - context._add_to_context(*curve.edges(), mode=mode) + if context is not None: + context._add_to_context(*curve.edges(), mode=mode) if isinstance(curve, Edge): super().__init__(Wire.make_wire([curve]).wrapped) @@ -268,7 +279,8 @@ class Bezier(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) polls = WorkplaneList.localize(*cntl_pnts) curve = Edge.make_bezier(*polls, weights=weights) @@ -300,7 +312,8 @@ class CenterArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) center_point = WorkplaneList.localize(center) circle_workplane = copy.copy(WorkplaneList._get_context().workplanes[0]) @@ -484,7 +497,8 @@ class EllipticalCenterArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) center_pnt = WorkplaneList.localize(center) ellipse_workplane = copy.copy(WorkplaneList._get_context().workplanes[0]) @@ -533,7 +547,8 @@ class Helix(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) center_pnt = WorkplaneList.localize(center) helix = Wire.make_helix( @@ -566,7 +581,8 @@ class JernArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) start = WorkplaneList.localize(start) self.start = start @@ -602,11 +618,13 @@ class Line(BaseLineObject): _applies_to = [BuildLine._tag()] def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADD): - context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) - if len(pts) != 2: raise ValueError("Line requires two pts") + + context: BuildLine = BuildLine._get_context(self) + if context is not None: + context.validate_inputs(self) + pts = WorkplaneList.localize(*pts) lines_pts = [Vector(p) for p in pts] @@ -644,7 +662,8 @@ class PolarLine(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) start = WorkplaneList.localize(start) if direction: @@ -692,7 +711,8 @@ class Polyline(BaseLineObject): def __init__(self, *pts: VectorLike, close: bool = False, mode: Mode = Mode.ADD): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) if len(pts) < 3: raise ValueError("polyline requires three or more pts") @@ -734,7 +754,8 @@ class RadiusArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) start, end = WorkplaneList.localize(start_point, end_point) # Calculate the sagitta from the radius @@ -777,7 +798,8 @@ class SagittaArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) start, end = WorkplaneList.localize(start_point, end_point) mid_point = (end + start) * 0.5 @@ -819,7 +841,8 @@ class Spline(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) spline_pts = WorkplaneList.localize(*pts) @@ -875,7 +898,8 @@ class TangentArc(BaseLineObject): mode: Mode = Mode.ADD, ): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) if len(pts) != 2: raise ValueError("tangent_arc requires two points") @@ -907,7 +931,8 @@ class ThreePointArc(BaseLineObject): def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADD): context: BuildLine = BuildLine._get_context(self) - context.validate_inputs(self) + if context is not None: + context.validate_inputs(self) if len(pts) != 3: raise ValueError("ThreePointArc requires three points") diff --git a/src/build123d/build_part.py b/src/build123d/build_part.py index f8f6472..9c8a10e 100644 --- a/src/build123d/build_part.py +++ b/src/build123d/build_part.py @@ -300,9 +300,7 @@ class BasePartObject(Part): ] context._add_to_context(*new_solids, mode=mode) - super().__init__( - Compound.make_compound(new_solids).wrapped, is_alg=(context == None) - ) + super().__init__(Compound.make_compound(new_solids).wrapped) # diff --git a/src/build123d/build_sketch.py b/src/build123d/build_sketch.py index 3482fd1..3295634 100644 --- a/src/build123d/build_sketch.py +++ b/src/build123d/build_sketch.py @@ -340,9 +340,7 @@ class BaseSketchObject(Sketch): ] context._add_to_context(*new_faces, mode=mode) - super().__init__( - Compound.make_compound(new_faces).wrapped, is_alg=(context == None) - ) + super().__init__(Compound.make_compound(new_faces).wrapped) class Circle(BaseSketchObject): diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py index fa56084..4507161 100644 --- a/src/build123d/geometry.py +++ b/src/build123d/geometry.py @@ -1667,7 +1667,7 @@ class Plane: if isinstance(other, Location): return Plane(self.to_location() * other) - elif hasattr(other, "wrapped"): # Shape + elif hasattr(other, "wrapped") and not isinstance(other, Vector): # Shape return self.to_location() * other else: diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 57d1c97..f21e4fa 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -1098,7 +1098,6 @@ class Shape(NodeMixin): joints: dict[str, Joint] = None, parent: Compound = None, children: list[Shape] = None, - is_alg: bool = True, ): self.wrapped = downcast(obj) if obj else None self.for_construction = False @@ -1118,6 +1117,10 @@ class Shape(NodeMixin): # parent must be set following children as post install accesses children self.parent = parent + # Faces can optionally record the plane it was created on for later extrusion + if isinstance(self, Face): + self.created_on: Plane = None + @property def location(self) -> Location: """Get this Shape's Location""" @@ -2861,9 +2864,10 @@ class ShapeList(list[T]): return ShapeList(list(self) + list(other)) def __sub__(self, other: ShapeList) -> ShapeList: - hash_other = [hash(o) for o in other] - hash_set = {hash(o): o for o in self if hash(o) not in hash_other} - return ShapeList(hash_set.values()) + # hash_other = [hash(o) for o in other] + # hash_set = {hash(o): o for o in self if hash(o) not in hash_other} + # return ShapeList(hash_set.values()) + return ShapeList(set(self) - set(other)) def __getitem__(self, key): """Return slices of ShapeList as ShapeList""" diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 33cb6e1..92e3c60 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -1861,7 +1861,7 @@ class TestPlane(DirectApiTestCase): self.assertVectorAlmostEquals( p2.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 ) - with self.assertRaises(AttributeError): + with self.assertRaises(TypeError): p2 * Vector(1, 1, 1) def test_plane_methods(self):