From 80acfa3e0dd0fbfe48fc15922cc8ac90511d9c69 Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Thu, 7 Nov 2024 23:19:38 +0100 Subject: [PATCH 1/5] Added thicken and new split tools --- src/build123d/topology.py | 84 ++++++++++++++++++++++++--------------- tests/test_direct_api.py | 29 ++++++++++++++ 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 2399846..1b80735 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -366,6 +366,7 @@ geom_LUT_EDGE: Dict[ga.GeomAbs_CurveType, GeomType] = { Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"] +TrimmingTool = Union[Plane,"Shell", "Face"] def tuplify(obj: Any, dim: int) -> tuple: """Create a size tuple""" @@ -2765,7 +2766,7 @@ class Shape(NodeMixin): return ShapeList([Face(face) for face in faces]) - def split(self, surface: Union[Plane, Face], keep: Keep = Keep.TOP) -> Self: + def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP) -> Self: """split Split this shape by the provided plane or face. @@ -2781,13 +2782,9 @@ class Shape(NodeMixin): shape_list.Append(self.wrapped) # Define the splitting tool - tool = ( - Face.make_plane(surface).wrapped - if isinstance(surface, Plane) - else surface.wrapped - ) + trim_tool = Face.make_plane(tool).wrapped if isinstance(tool, Plane) else tool.wrapped tool_list = TopTools_ListOfShape() - tool_list.Append(tool) + tool_list.Append(trim_tool) # Create the splitter algorithm splitter = BRepAlgoAPI_Splitter() @@ -2801,13 +2798,13 @@ class Shape(NodeMixin): result = Compound(downcast(splitter.Shape())).unwrap(fully=False) if keep != Keep.BOTH: - if not isinstance(surface, Plane): + if not isinstance(tool, Plane): # Create solids from the surfaces for sorting - surface_up = surface.thicken(0.1) + surface_up = tool.thicken(0.1) tops, bottoms = [], [] for part in result: - if isinstance(surface, Plane): - is_up = surface.to_local_coords(part).center().Z >= 0 + if isinstance(tool, Plane): + is_up = tool.to_local_coords(part).center().Z >= 0 else: is_up = surface_up.intersect(part).volume >= TOLERANCE (tops if is_up else bottoms).append(part) @@ -6484,7 +6481,7 @@ class Face(Shape): and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE ) - def thicken(self, depth: float, normal_override: VectorLike = None) -> Solid: + def thicken(self, depth: float, normal_override: Optional[VectorLike] = None) -> Solid: """Thicken Face Create a solid from a potentially non planar face by thickening along the normals. @@ -6514,27 +6511,7 @@ class Face(Shape): if face_normal.dot(Vector(normal_override).normalized()) < 0: adjusted_depth = -depth - solid = BRepOffset_MakeOffset() - solid.Initialize( - self.wrapped, - Offset=adjusted_depth, - Tol=1.0e-5, - Mode=BRepOffset_Skin, - # BRepOffset_RectoVerso - which describes the offset of a given surface shell along both - # sides of the surface but doesn't seem to work - Intersection=True, - SelfInter=False, - Join=GeomAbs_Intersection, # Could be GeomAbs_Arc,GeomAbs_Tangent,GeomAbs_Intersection - Thickening=True, - RemoveIntEdges=True, - ) - solid.MakeOffsetShape() - try: - result = Solid(solid.Shape()) - except StdFail_NotDone as err: - raise RuntimeError("Error applying thicken to given Face") from err - - return result.clean() + return _thicken(self.wrapped, adjusted_depth) def project_to_shape( self, target_object: Shape, direction: VectorLike, taper: float = 0 @@ -7000,6 +6977,25 @@ class Shell(Shape): """ return cls(_make_loft(objs, False, ruled)) + def thicken(self, depth: float) -> Solid: + """Thicken Shell + + Create a solid from a shell by thickening along the normals. + + Args: + depth (float): Amount to thicken face(s), can be positive or negative. + normal_override (Vector, optional): The normal_override vector can be used to + indicate which way is 'up', potentially flipping the face normal direction + such that many faces with different normals all go in the same direction + (direction need only be +/- 90 degrees from the face normal). Defaults to None. + + Raises: + RuntimeError: Opencascade internal failures + + Returns: + Solid: The resulting Solid object + """ + return _thicken(self.wrapped, depth) class Solid(Mixin3D, Shape): """A Solid in build123d represents a three-dimensional solid geometry @@ -8860,6 +8856,28 @@ class Joint(ABC): """A CAD object positioned in global space to illustrate the joint""" raise NotImplementedError +def _thicken(obj: TopoDS_Shape, depth: float): + solid = BRepOffset_MakeOffset() + solid.Initialize( + obj, + Offset=depth, + Tol=1.0e-5, + Mode=BRepOffset_Skin, + # BRepOffset_RectoVerso - which describes the offset of a given surface shell along both + # sides of the surface but doesn't seem to work + Intersection=True, + SelfInter=False, + Join=GeomAbs_Intersection, # Could be GeomAbs_Arc,GeomAbs_Tangent,GeomAbs_Intersection + Thickening=True, + RemoveIntEdges=True, + ) + solid.MakeOffsetShape() + try: + result = Solid(solid.Shape()) + except StdFail_NotDone as err: + raise RuntimeError("Error applying thicken to given Face") from err + + return result.clean() def _make_loft( objs: Iterable[Union[Vertex, Wire]], diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 413a135..9043f40 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -3004,6 +3004,24 @@ class TestShape(DirectApiTestCase): self.assertLess(s2.volume, s.volume) self.assertGreater(s2.volume, 0.0) + def test_split_by_non_plarnar_face(self): + box = Solid.make_box(1, 1, 1) + tool = Circle(1).wire() + tool_shell: Shell = Shape.extrude(tool, Vector(0, 0, 1)) + split = box.split(tool_shell, keep=Keep.BOTH) + + self.assertEqual(len(split.solids()), 2) + self.assertGreater(split.solids()[0].volume, split.solids()[1].volume) + + def test_split_by_shell(self): + box = Solid.make_box(5, 5, 1) + tool = Wire.make_rect(4,4) + tool_shell: Shell = Shape.extrude(tool, Vector(0, 0, 1)) + split = box.split(tool_shell, keep=Keep.TOP) + inner_vol = 2*2 + outer_vol = 5*5 + self.assertAlmostEqual(split.volume , outer_vol-inner_vol) + def test_split_by_perimeter(self): # Test 0 - extract a spherical cap target0 = Solid.make_sphere(10).rotate(Axis.Z, 90) @@ -3697,6 +3715,17 @@ class TestShells(DirectApiTestCase): cylinder_area = 2 * math.pi * r * h self.assertAlmostEqual(loft.area, cylinder_area) + def test_thicken(self): + rect = Wire.make_rect(10, 5) + shell: Shell = Shape.extrude(rect, Vector(0, 0, 3)) + thick = shell.thicken(1) + + self.assertEqual(isinstance(thick, Solid), True) + inner_vol = 3 * 10 * 5 + outer_vol = 3 * 12 * 7 + self.assertAlmostEqual(thick.volume, outer_vol - inner_vol) + + class TestSolid(DirectApiTestCase): def test_make_solid(self): box_faces = Solid.make_box(1, 1, 1).faces() From f2d4524571e38436a1f19c32c748a5325cce9a3e Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Fri, 8 Nov 2024 22:15:26 +0100 Subject: [PATCH 2/5] re ran black --- src/build123d/topology.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 1b80735..37f4142 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -366,7 +366,8 @@ geom_LUT_EDGE: Dict[ga.GeomAbs_CurveType, GeomType] = { Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"] -TrimmingTool = Union[Plane,"Shell", "Face"] +TrimmingTool = Union[Plane, "Shell", "Face"] + def tuplify(obj: Any, dim: int) -> tuple: """Create a size tuple""" @@ -2782,7 +2783,9 @@ class Shape(NodeMixin): shape_list.Append(self.wrapped) # Define the splitting tool - trim_tool = Face.make_plane(tool).wrapped if isinstance(tool, Plane) else tool.wrapped + trim_tool = ( + Face.make_plane(tool).wrapped if isinstance(tool, Plane) else tool.wrapped + ) tool_list = TopTools_ListOfShape() tool_list.Append(trim_tool) @@ -6481,7 +6484,9 @@ class Face(Shape): and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE ) - def thicken(self, depth: float, normal_override: Optional[VectorLike] = None) -> Solid: + def thicken( + self, depth: float, normal_override: Optional[VectorLike] = None + ) -> Solid: """Thicken Face Create a solid from a potentially non planar face by thickening along the normals. @@ -6997,6 +7002,7 @@ class Shell(Shape): """ return _thicken(self.wrapped, depth) + class Solid(Mixin3D, Shape): """A Solid in build123d represents a three-dimensional solid geometry in a topological structure. A solid is a closed and bounded volume, enclosing @@ -8856,6 +8862,7 @@ class Joint(ABC): """A CAD object positioned in global space to illustrate the joint""" raise NotImplementedError + def _thicken(obj: TopoDS_Shape, depth: float): solid = BRepOffset_MakeOffset() solid.Initialize( @@ -8879,6 +8886,7 @@ def _thicken(obj: TopoDS_Shape, depth: float): return result.clean() + def _make_loft( objs: Iterable[Union[Vertex, Wire]], filled: bool, From 4fad571781e972031d3647d660eafa91dc4d58dc Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Fri, 8 Nov 2024 22:18:09 +0100 Subject: [PATCH 3/5] black on test file --- tests/test_direct_api.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 9043f40..fb1b161 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -3015,12 +3015,12 @@ class TestShape(DirectApiTestCase): def test_split_by_shell(self): box = Solid.make_box(5, 5, 1) - tool = Wire.make_rect(4,4) + tool = Wire.make_rect(4, 4) tool_shell: Shell = Shape.extrude(tool, Vector(0, 0, 1)) split = box.split(tool_shell, keep=Keep.TOP) - inner_vol = 2*2 - outer_vol = 5*5 - self.assertAlmostEqual(split.volume , outer_vol-inner_vol) + inner_vol = 2 * 2 + outer_vol = 5 * 5 + self.assertAlmostEqual(split.volume, outer_vol - inner_vol) def test_split_by_perimeter(self): # Test 0 - extract a spherical cap @@ -3866,7 +3866,14 @@ class TestSolid(DirectApiTestCase): Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) with self.assertRaises(ValueError): - Solid.make_loft([Vertex(0, 0, 1),Wire.make_rect(1, 1), Vertex(0, 0, 2), Vertex(0, 0, 3)]) + Solid.make_loft( + [ + Vertex(0, 0, 1), + Wire.make_rect(1, 1), + Vertex(0, 0, 2), + Vertex(0, 0, 3), + ] + ) def test_extrude_until(self): square = Face.make_rect(1, 1) From 63ca9311ce63503525fc971aff983f2e19026f62 Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Fri, 8 Nov 2024 22:19:06 +0100 Subject: [PATCH 4/5] typo --- tests/test_direct_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index fb1b161..1f70ac9 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -3004,7 +3004,7 @@ class TestShape(DirectApiTestCase): self.assertLess(s2.volume, s.volume) self.assertGreater(s2.volume, 0.0) - def test_split_by_non_plarnar_face(self): + def test_split_by_non_planar_face(self): box = Solid.make_box(1, 1, 1) tool = Circle(1).wire() tool_shell: Shell = Shape.extrude(tool, Vector(0, 0, 1)) From f13107d0fa6540f39c5b8e20faa776aba198dd70 Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Sun, 10 Nov 2024 18:57:16 +0100 Subject: [PATCH 5/5] Fix doc --- src/build123d/topology.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 37f4142..28a029e 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -6989,10 +6989,6 @@ class Shell(Shape): Args: depth (float): Amount to thicken face(s), can be positive or negative. - normal_override (Vector, optional): The normal_override vector can be used to - indicate which way is 'up', potentially flipping the face normal direction - such that many faces with different normals all go in the same direction - (direction need only be +/- 90 degrees from the face normal). Defaults to None. Raises: RuntimeError: Opencascade internal failures