Merge pull request #771 from Jojain/dev

Added thicken and new split tools
This commit is contained in:
Roger Maitland 2024-11-10 20:43:31 -05:00 committed by GitHub
commit a353d3e776
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 33 deletions

View file

@ -366,6 +366,8 @@ geom_LUT_EDGE: Dict[ga.GeomAbs_CurveType, GeomType] = {
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"] Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
TrimmingTool = Union[Plane, "Shell", "Face"]
def tuplify(obj: Any, dim: int) -> tuple: def tuplify(obj: Any, dim: int) -> tuple:
"""Create a size tuple""" """Create a size tuple"""
@ -2775,7 +2777,7 @@ class Shape(NodeMixin):
return ShapeList([Face(face) for face in faces]) 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
Split this shape by the provided plane or face. Split this shape by the provided plane or face.
@ -2791,13 +2793,11 @@ class Shape(NodeMixin):
shape_list.Append(self.wrapped) shape_list.Append(self.wrapped)
# Define the splitting tool # Define the splitting tool
tool = ( trim_tool = (
Face.make_plane(surface).wrapped Face.make_plane(tool).wrapped if isinstance(tool, Plane) else tool.wrapped
if isinstance(surface, Plane)
else surface.wrapped
) )
tool_list = TopTools_ListOfShape() tool_list = TopTools_ListOfShape()
tool_list.Append(tool) tool_list.Append(trim_tool)
# Create the splitter algorithm # Create the splitter algorithm
splitter = BRepAlgoAPI_Splitter() splitter = BRepAlgoAPI_Splitter()
@ -2811,13 +2811,13 @@ class Shape(NodeMixin):
result = Compound(downcast(splitter.Shape())).unwrap(fully=False) result = Compound(downcast(splitter.Shape())).unwrap(fully=False)
if keep != Keep.BOTH: if keep != Keep.BOTH:
if not isinstance(surface, Plane): if not isinstance(tool, Plane):
# Create solids from the surfaces for sorting # Create solids from the surfaces for sorting
surface_up = surface.thicken(0.1) surface_up = tool.thicken(0.1)
tops, bottoms = [], [] tops, bottoms = [], []
for part in result: for part in result:
if isinstance(surface, Plane): if isinstance(tool, Plane):
is_up = surface.to_local_coords(part).center().Z >= 0 is_up = tool.to_local_coords(part).center().Z >= 0
else: else:
is_up = surface_up.intersect(part).volume >= TOLERANCE is_up = surface_up.intersect(part).volume >= TOLERANCE
(tops if is_up else bottoms).append(part) (tops if is_up else bottoms).append(part)
@ -6490,7 +6490,9 @@ class Face(Shape):
and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE 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 """Thicken Face
Create a solid from a potentially non planar face by thickening along the normals. Create a solid from a potentially non planar face by thickening along the normals.
@ -6520,27 +6522,7 @@ class Face(Shape):
if face_normal.dot(Vector(normal_override).normalized()) < 0: if face_normal.dot(Vector(normal_override).normalized()) < 0:
adjusted_depth = -depth adjusted_depth = -depth
solid = BRepOffset_MakeOffset() return _thicken(self.wrapped, adjusted_depth)
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()
def project_to_shape( def project_to_shape(
self, target_object: Shape, direction: VectorLike, taper: float = 0 self, target_object: Shape, direction: VectorLike, taper: float = 0
@ -7006,6 +6988,22 @@ class Shell(Shape):
""" """
return cls(_make_loft(objs, False, ruled)) 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.
Raises:
RuntimeError: Opencascade internal failures
Returns:
Solid: The resulting Solid object
"""
return _thicken(self.wrapped, depth)
class Solid(Mixin3D, Shape): class Solid(Mixin3D, Shape):
"""A Solid in build123d represents a three-dimensional solid geometry """A Solid in build123d represents a three-dimensional solid geometry
@ -8867,6 +8865,30 @@ class Joint(ABC):
raise NotImplementedError 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( def _make_loft(
objs: Iterable[Union[Vertex, Wire]], objs: Iterable[Union[Vertex, Wire]],
filled: bool, filled: bool,

View file

@ -3039,6 +3039,24 @@ class TestShape(DirectApiTestCase):
self.assertLess(s2.volume, s.volume) self.assertLess(s2.volume, s.volume)
self.assertGreater(s2.volume, 0.0) self.assertGreater(s2.volume, 0.0)
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))
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): def test_split_by_perimeter(self):
# Test 0 - extract a spherical cap # Test 0 - extract a spherical cap
target0 = Solid.make_sphere(10).rotate(Axis.Z, 90) target0 = Solid.make_sphere(10).rotate(Axis.Z, 90)
@ -3722,6 +3740,17 @@ class TestShells(DirectApiTestCase):
cylinder_area = 2 * math.pi * r * h cylinder_area = 2 * math.pi * r * h
self.assertAlmostEqual(loft.area, cylinder_area) 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): class TestSolid(DirectApiTestCase):
def test_make_solid(self): def test_make_solid(self):
box_faces = Solid.make_box(1, 1, 1).faces() box_faces = Solid.make_box(1, 1, 1).faces()
@ -3862,7 +3891,14 @@ class TestSolid(DirectApiTestCase):
Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)])
with self.assertRaises(ValueError): 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): def test_extrude_until(self):
square = Face.make_rect(1, 1) square = Face.make_rect(1, 1)