mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-05 18:20:46 -08:00
Refactored Solid.extrude_until, moved split to Shape, fixed misc typing problems
This commit is contained in:
parent
a8fc16b344
commit
2fa0dd22da
9 changed files with 449 additions and 342 deletions
|
|
@ -47,9 +47,10 @@ with BuildPart() as ppp109:
|
|||
split(bisect_by=Plane.YZ)
|
||||
extrude(amount=6)
|
||||
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
||||
# extrude(f, until=Until.NEXT) # throws a warning
|
||||
extrude(f, amount=10)
|
||||
fillet(ppp109.edge(Select.NEW), 16)
|
||||
extrude(f, until=Until.NEXT)
|
||||
fillet(ppp109.edges().filter_by(Axis.Y).sort_by(Axis.Z)[2], 16)
|
||||
# extrude(f, amount=10)
|
||||
# fillet(ppp109.edges(Select.NEW), 16)
|
||||
|
||||
|
||||
show(ppp109)
|
||||
|
|
@ -59,4 +60,4 @@ want_mass = 307.23
|
|||
tolerance = 1
|
||||
delta = abs(got_mass - want_mass)
|
||||
print(f"Mass: {got_mass:0.2f} g")
|
||||
assert delta < tolerance, f'{got_mass=}, {want_mass=}, {delta=}, {tolerance=}'
|
||||
assert delta < tolerance, f"{got_mass=}, {want_mass=}, {delta=}, {tolerance=}"
|
||||
|
|
|
|||
|
|
@ -223,8 +223,8 @@ def extrude(
|
|||
|
||||
new_solids.append(
|
||||
Solid.extrude_until(
|
||||
section=face,
|
||||
target_object=target_object,
|
||||
face,
|
||||
target=target_object,
|
||||
direction=plane.z_dir * direction,
|
||||
until=until,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -233,11 +233,11 @@ from .shape_core import (
|
|||
shapetype,
|
||||
topods_dim,
|
||||
unwrap_topods_compound,
|
||||
_topods_bool_op,
|
||||
)
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
_make_topods_face_from_wires,
|
||||
_topods_bool_op,
|
||||
isclose_b,
|
||||
)
|
||||
from .zero_d import Vertex, topo_explore_common_vertex
|
||||
|
|
@ -1377,144 +1377,6 @@ class Mixin1D(Shape[TOPODS]):
|
|||
|
||||
return (visible_edges, hidden_edges)
|
||||
|
||||
@overload
|
||||
def split(
|
||||
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
|
||||
) -> Self | list[Self] | None:
|
||||
"""split and keep inside or outside"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
|
||||
"""split and return the unordered pieces"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
|
||||
Self | list[Self] | None,
|
||||
Self | list[Self] | None,
|
||||
]:
|
||||
"""split and keep inside and outside"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
|
||||
"""split and keep inside (default)"""
|
||||
|
||||
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
||||
"""split
|
||||
|
||||
Split this shape by the provided plane or face.
|
||||
|
||||
Args:
|
||||
surface (Plane | Face): surface to segment shape
|
||||
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
|
||||
|
||||
Returns:
|
||||
Shape: result of split
|
||||
Returns:
|
||||
Self | list[Self] | None,
|
||||
Tuple[Self | list[Self] | None]: The result of the split operation.
|
||||
|
||||
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
|
||||
if no top is found.
|
||||
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
|
||||
if no bottom is found.
|
||||
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
|
||||
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
|
||||
"""
|
||||
if self._wrapped is None or not tool:
|
||||
raise ValueError("Can't split an empty edge/wire/tool")
|
||||
|
||||
shape_list = TopTools_ListOfShape()
|
||||
shape_list.Append(self.wrapped)
|
||||
|
||||
# Define the splitting tool
|
||||
trim_tool = (
|
||||
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
|
||||
if isinstance(tool, Plane)
|
||||
else tool.wrapped
|
||||
)
|
||||
tool_list = TopTools_ListOfShape()
|
||||
tool_list.Append(trim_tool)
|
||||
|
||||
# Create the splitter algorithm
|
||||
splitter = BRepAlgoAPI_Splitter()
|
||||
|
||||
# Set the shape to be split and the splitting tool (plane face)
|
||||
splitter.SetArguments(shape_list)
|
||||
splitter.SetTools(tool_list)
|
||||
|
||||
# Perform the splitting operation
|
||||
splitter.Build()
|
||||
|
||||
split_result = downcast(splitter.Shape())
|
||||
# Remove unnecessary TopoDS_Compound around single shape
|
||||
if isinstance(split_result, TopoDS_Compound):
|
||||
split_result = unwrap_topods_compound(split_result, True)
|
||||
|
||||
# For speed the user may just want all the objects which they
|
||||
# can sort more efficiently then the generic algorithm below
|
||||
if keep == Keep.ALL:
|
||||
return ShapeList(
|
||||
self.__class__.cast(part)
|
||||
for part in get_top_level_topods_shapes(split_result)
|
||||
)
|
||||
|
||||
if not isinstance(tool, Plane):
|
||||
# Get a TopoDS_Face to work with from the tool
|
||||
if isinstance(trim_tool, TopoDS_Shell):
|
||||
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
|
||||
tool_face = TopoDS.Face_s(face_explorer.Current())
|
||||
else:
|
||||
tool_face = trim_tool
|
||||
|
||||
# Create a reference point off the +ve side of the tool
|
||||
surface_gppnt = gp_Pnt()
|
||||
surface_normal = gp_Vec()
|
||||
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
|
||||
BRepGProp_Face(tool_face).Normal(
|
||||
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
|
||||
)
|
||||
normalized_surface_normal = Vector(
|
||||
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
|
||||
).normalized()
|
||||
surface_point = Vector(surface_gppnt)
|
||||
ref_point = surface_point + normalized_surface_normal
|
||||
|
||||
# Create a HalfSpace - Solidish object to determine top/bottom
|
||||
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
|
||||
# mypy expects only a TopoDS_Shell here
|
||||
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
|
||||
# type: ignore
|
||||
tool_solid = half_space_maker.Solid()
|
||||
|
||||
tops: list[Shape] = []
|
||||
bottoms: list[Shape] = []
|
||||
properties = GProp_GProps()
|
||||
for part in get_top_level_topods_shapes(split_result):
|
||||
sub_shape = self.__class__.cast(part)
|
||||
if isinstance(tool, Plane):
|
||||
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
|
||||
else:
|
||||
# Intersect self and the thickened tool
|
||||
is_up_obj = _topods_bool_op(
|
||||
(part,), (tool_solid,), BRepAlgoAPI_Common()
|
||||
)
|
||||
# Check for valid intersections
|
||||
BRepGProp.LinearProperties_s(is_up_obj, properties)
|
||||
# Mass represents the total length for linear properties
|
||||
is_up = properties.Mass() >= TOLERANCE
|
||||
(tops if is_up else bottoms).append(sub_shape)
|
||||
|
||||
top = None if not tops else tops[0] if len(tops) == 1 else tops
|
||||
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
|
||||
|
||||
if keep == Keep.BOTH:
|
||||
return (top, bottom)
|
||||
if keep == Keep.TOP:
|
||||
return top
|
||||
if keep == Keep.BOTTOM:
|
||||
return bottom
|
||||
return None
|
||||
|
||||
def start_point(self) -> Vector:
|
||||
"""The start point of this edge
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ from OCP.BRepFeat import BRepFeat_SplitShape
|
|||
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
|
||||
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||
from OCP.BRepTools import BRepTools
|
||||
from OCP.gce import gce_MakeLin
|
||||
from OCP.Geom import Geom_Line
|
||||
|
|
@ -196,7 +197,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
ta.TopAbs_COMPSOLID: "CompSolid",
|
||||
}
|
||||
|
||||
shape_properties_LUT = {
|
||||
shape_properties_LUT: dict[TopAbs_ShapeEnum:function] = {
|
||||
ta.TopAbs_VERTEX: None,
|
||||
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
||||
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
|
||||
|
|
@ -1786,6 +1787,144 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"""solids - all the solids in this Shape"""
|
||||
return ShapeList()
|
||||
|
||||
@overload
|
||||
def split(
|
||||
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
|
||||
) -> Self | list[Self] | None:
|
||||
"""split and keep inside or outside"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
|
||||
"""split and return the unordered pieces"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
|
||||
Self | list[Self] | None,
|
||||
Self | list[Self] | None,
|
||||
]:
|
||||
"""split and keep inside and outside"""
|
||||
|
||||
@overload
|
||||
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
|
||||
"""split and keep inside (default)"""
|
||||
|
||||
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
||||
"""split
|
||||
|
||||
Split this shape by the provided plane or face.
|
||||
|
||||
Args:
|
||||
surface (Plane | Face): surface to segment shape
|
||||
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
|
||||
|
||||
Returns:
|
||||
Shape: result of split
|
||||
Returns:
|
||||
Self | list[Self] | None,
|
||||
Tuple[Self | list[Self] | None]: The result of the split operation.
|
||||
|
||||
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
|
||||
if no top is found.
|
||||
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
|
||||
if no bottom is found.
|
||||
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
|
||||
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
|
||||
"""
|
||||
if self._wrapped is None or not tool:
|
||||
raise ValueError("Can't split an empty edge/wire/tool")
|
||||
|
||||
shape_list = TopTools_ListOfShape()
|
||||
shape_list.Append(self.wrapped)
|
||||
|
||||
# Define the splitting tool
|
||||
trim_tool = (
|
||||
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
|
||||
if isinstance(tool, Plane)
|
||||
else tool.wrapped
|
||||
)
|
||||
tool_list = TopTools_ListOfShape()
|
||||
tool_list.Append(trim_tool)
|
||||
|
||||
# Create the splitter algorithm
|
||||
splitter = BRepAlgoAPI_Splitter()
|
||||
|
||||
# Set the shape to be split and the splitting tool (plane face)
|
||||
splitter.SetArguments(shape_list)
|
||||
splitter.SetTools(tool_list)
|
||||
|
||||
# Perform the splitting operation
|
||||
splitter.Build()
|
||||
|
||||
split_result = downcast(splitter.Shape())
|
||||
# Remove unnecessary TopoDS_Compound around single shape
|
||||
if isinstance(split_result, TopoDS_Compound):
|
||||
split_result = unwrap_topods_compound(split_result, True)
|
||||
|
||||
# For speed the user may just want all the objects which they
|
||||
# can sort more efficiently then the generic algorithm below
|
||||
if keep == Keep.ALL:
|
||||
return ShapeList(
|
||||
self.__class__.cast(part)
|
||||
for part in get_top_level_topods_shapes(split_result)
|
||||
)
|
||||
|
||||
if not isinstance(tool, Plane):
|
||||
# Get a TopoDS_Face to work with from the tool
|
||||
if isinstance(trim_tool, TopoDS_Shell):
|
||||
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
|
||||
tool_face = TopoDS.Face_s(face_explorer.Current())
|
||||
else:
|
||||
tool_face = trim_tool
|
||||
|
||||
# Create a reference point off the +ve side of the tool
|
||||
surface_gppnt = gp_Pnt()
|
||||
surface_normal = gp_Vec()
|
||||
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
|
||||
BRepGProp_Face(tool_face).Normal(
|
||||
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
|
||||
)
|
||||
normalized_surface_normal = Vector(
|
||||
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
|
||||
).normalized()
|
||||
surface_point = Vector(surface_gppnt)
|
||||
ref_point = surface_point + normalized_surface_normal
|
||||
|
||||
# Create a HalfSpace - Solidish object to determine top/bottom
|
||||
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
|
||||
# mypy expects only a TopoDS_Shell here
|
||||
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
|
||||
# type: ignore
|
||||
tool_solid = half_space_maker.Solid()
|
||||
|
||||
tops: list[Shape] = []
|
||||
bottoms: list[Shape] = []
|
||||
properties = GProp_GProps()
|
||||
for part in get_top_level_topods_shapes(split_result):
|
||||
sub_shape = self.__class__.cast(part)
|
||||
if isinstance(tool, Plane):
|
||||
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
|
||||
else:
|
||||
# Intersect self and the thickened tool
|
||||
is_up_obj = _topods_bool_op(
|
||||
(part,), (tool_solid,), BRepAlgoAPI_Common()
|
||||
)
|
||||
# Check for valid intersections
|
||||
BRepGProp.LinearProperties_s(is_up_obj, properties)
|
||||
# Mass represents the total length for linear properties
|
||||
is_up = properties.Mass() >= TOLERANCE
|
||||
(tops if is_up else bottoms).append(sub_shape)
|
||||
|
||||
top = None if not tops else tops[0] if len(tops) == 1 else tops
|
||||
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
|
||||
|
||||
if keep == Keep.BOTH:
|
||||
return (top, bottom)
|
||||
if keep == Keep.TOP:
|
||||
return top
|
||||
if keep == Keep.BOTTOM:
|
||||
return bottom
|
||||
return None
|
||||
|
||||
@overload
|
||||
def split_by_perimeter(
|
||||
self, perimeter: Edge | Wire, keep: Literal[Keep.INSIDE, Keep.OUTSIDE]
|
||||
|
|
@ -3011,6 +3150,45 @@ def _sew_topods_faces(faces: Iterable[TopoDS_Face]) -> TopoDS_Shape:
|
|||
return downcast(shell_builder.SewedShape())
|
||||
|
||||
|
||||
def _topods_bool_op(
|
||||
args: Iterable[TopoDS_Shape],
|
||||
tools: Iterable[TopoDS_Shape],
|
||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
|
||||
) -> TopoDS_Shape:
|
||||
"""Generic boolean operation for TopoDS_Shapes
|
||||
|
||||
Args:
|
||||
args: Iterable[TopoDS_Shape]:
|
||||
tools: Iterable[TopoDS_Shape]:
|
||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
|
||||
|
||||
Returns: TopoDS_Shape
|
||||
|
||||
"""
|
||||
args = list(args)
|
||||
tools = list(tools)
|
||||
arg = TopTools_ListOfShape()
|
||||
for obj in args:
|
||||
arg.Append(obj)
|
||||
|
||||
tool = TopTools_ListOfShape()
|
||||
for obj in tools:
|
||||
tool.Append(obj)
|
||||
|
||||
operation.SetArguments(arg)
|
||||
operation.SetTools(tool)
|
||||
|
||||
operation.SetRunParallel(True)
|
||||
operation.Build()
|
||||
|
||||
result = downcast(operation.Shape())
|
||||
# Remove unnecessary TopoDS_Compound around single shape
|
||||
if isinstance(result, TopoDS_Compound):
|
||||
result = unwrap_topods_compound(result, True)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
||||
out = {} # using dict to prevent duplicates
|
||||
|
|
|
|||
|
|
@ -54,11 +54,10 @@ license:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
from math import radians, cos, tan
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
from typing import cast as tcast
|
||||
from typing_extensions import Self
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
|
|
@ -86,13 +85,20 @@ from OCP.GProp import GProp_GProps
|
|||
from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
|
||||
from OCP.LocOpe import LocOpe_DPrism
|
||||
from OCP.ShapeFix import ShapeFix_Solid
|
||||
from OCP.Standard import Standard_Failure
|
||||
from OCP.Standard import Standard_Failure, Standard_TypeMismatch
|
||||
from OCP.StdFail import StdFail_NotDone
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopExp import TopExp, TopExp_Explorer
|
||||
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
|
||||
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire
|
||||
from OCP.TopoDS import (
|
||||
TopoDS,
|
||||
TopoDS_Face,
|
||||
TopoDS_Shape,
|
||||
TopoDS_Shell,
|
||||
TopoDS_Solid,
|
||||
TopoDS_Wire,
|
||||
)
|
||||
from OCP.gp import gp_Ax2, gp_Pnt
|
||||
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
||||
from build123d.build_enums import CenterOf, GeomType, Keep, Kind, Transition, Until
|
||||
from build123d.geometry import (
|
||||
DEG2RAD,
|
||||
TOLERANCE,
|
||||
|
|
@ -107,7 +113,19 @@ from build123d.geometry import (
|
|||
)
|
||||
|
||||
from .one_d import Edge, Wire, Mixin1D
|
||||
from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
|
||||
from .shape_core import (
|
||||
TOPODS,
|
||||
Shape,
|
||||
ShapeList,
|
||||
Joint,
|
||||
TrimmingTool,
|
||||
downcast,
|
||||
shapetype,
|
||||
_sew_topods_faces,
|
||||
get_top_level_topods_shapes,
|
||||
unwrap_topods_compound,
|
||||
)
|
||||
|
||||
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
|
|
@ -126,7 +144,6 @@ class Mixin3D(Shape[TOPODS]):
|
|||
"""Additional methods to add to 3D Shape classes"""
|
||||
|
||||
project_to_viewport = Mixin1D.project_to_viewport
|
||||
split = Mixin1D.split
|
||||
find_intersection_points = Mixin2D.find_intersection_points
|
||||
|
||||
vertices = Mixin1D.vertices
|
||||
|
|
@ -507,7 +524,9 @@ class Mixin3D(Shape[TOPODS]):
|
|||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
|
||||
operation = BRepAlgoAPI_Section()
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
|
||||
BRepAlgoAPI_Section()
|
||||
)
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
if (
|
||||
not isinstance(obj, Edge | Wire)
|
||||
|
|
@ -610,8 +629,10 @@ class Mixin3D(Shape[TOPODS]):
|
|||
try:
|
||||
new_shape = self.__class__(fillet_builder.Shape())
|
||||
if not new_shape.is_valid:
|
||||
raise fillet_exception
|
||||
except fillet_exception:
|
||||
# raise fillet_exception
|
||||
raise Standard_Failure
|
||||
# except fillet_exception:
|
||||
except (Standard_Failure, StdFail_NotDone):
|
||||
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
||||
|
||||
# These numbers work, are they close enough? - if not try larger window
|
||||
|
|
@ -630,10 +651,10 @@ class Mixin3D(Shape[TOPODS]):
|
|||
|
||||
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
|
||||
# specific exceptions are required.
|
||||
if platform.system() == "Darwin":
|
||||
fillet_exception = Standard_Failure
|
||||
else:
|
||||
fillet_exception = StdFail_NotDone
|
||||
# if platform.system() == "Darwin":
|
||||
# fillet_exception = Standard_Failure
|
||||
# else:
|
||||
# fillet_exception = StdFail_NotDone
|
||||
|
||||
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
|
||||
|
||||
|
|
@ -892,7 +913,17 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
inner_comp = _make_topods_compound_from_shapes(inner_solids)
|
||||
|
||||
# subtract from the outer solid
|
||||
return Solid(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
|
||||
difference = BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape()
|
||||
|
||||
# convert to a TopoDS_Solid - might be wrapped in a TopoDS_Compound
|
||||
try:
|
||||
result = TopoDS.Solid_s(difference)
|
||||
except Standard_TypeMismatch:
|
||||
result = TopoDS.Solid_s(
|
||||
unwrap_topods_compound(TopoDS.Compound_s(difference), True)
|
||||
)
|
||||
|
||||
return Solid(result)
|
||||
|
||||
@classmethod
|
||||
def extrude_taper(
|
||||
|
|
@ -933,7 +964,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
direction.length / cos(radians(taper)),
|
||||
radians(taper),
|
||||
)
|
||||
new_solid = Solid(prism_builder.Shape())
|
||||
new_solid = Solid(TopoDS.Solid_s(prism_builder.Shape()))
|
||||
else:
|
||||
# Determine the offset to get the taper
|
||||
offset_amt = -direction.length * tan(radians(taper))
|
||||
|
|
@ -972,110 +1003,116 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
@classmethod
|
||||
def extrude_until(
|
||||
cls,
|
||||
section: Face,
|
||||
target_object: Compound | Solid,
|
||||
profile: Face,
|
||||
target: Compound | Solid,
|
||||
direction: VectorLike,
|
||||
until: Until = Until.NEXT,
|
||||
) -> Compound | Solid:
|
||||
) -> Solid:
|
||||
"""extrude_until
|
||||
|
||||
Extrude section in provided direction until it encounters either the
|
||||
NEXT or LAST surface of target_object. Note that the bounding surface
|
||||
must be larger than the extruded face where they contact.
|
||||
Extrude `profile` in the provided `direction` until it encounters a
|
||||
bounding surface on the `target`. The termination surface is chosen
|
||||
according to the `until` option:
|
||||
|
||||
* ``Until.NEXT`` — Extrude forward until the first intersecting surface.
|
||||
* ``Until.LAST`` — Extrude forward through all intersections, stopping at
|
||||
the farthest surface.
|
||||
* ``Until.PREVIOUS`` — Reverse the extrusion direction and stop at the
|
||||
first intersecting surface behind the profile.
|
||||
* ``Until.FIRST`` — Reverse the direction and stop at the farthest
|
||||
surface behind the profile.
|
||||
|
||||
When ``Until.PREVIOUS`` or ``Until.FIRST`` are used, the extrusion
|
||||
direction is automatically inverted before execution.
|
||||
|
||||
Note:
|
||||
The bounding surface on the target must be large enough to
|
||||
completely cover the extruded profile at the contact region.
|
||||
Partial overlaps may yield open or invalid solids.
|
||||
|
||||
Args:
|
||||
section (Face): Face to extrude
|
||||
target_object (Union[Compound, Solid]): object to limit extrusion
|
||||
direction (VectorLike): extrusion direction
|
||||
until (Until, optional): surface to limit extrusion. Defaults to Until.NEXT.
|
||||
profile (Face): The face to extrude.
|
||||
target (Union[Compound, Solid]): The object that limits the extrusion.
|
||||
direction (VectorLike): Extrusion direction.
|
||||
until (Until, optional): Surface selection mode controlling which
|
||||
intersection to stop at. Defaults to ``Until.NEXT``.
|
||||
|
||||
Raises:
|
||||
ValueError: provided face does not intersect target_object
|
||||
ValueError: If the provided profile does not intersect the target.
|
||||
|
||||
Returns:
|
||||
Union[Compound, Solid]: extruded Face
|
||||
Solid: The extruded and limited solid.
|
||||
"""
|
||||
direction = Vector(direction)
|
||||
if until in [Until.PREVIOUS, Until.FIRST]:
|
||||
direction *= -1
|
||||
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
||||
|
||||
max_dimension = find_max_dimension([section, target_object])
|
||||
clipping_direction = (
|
||||
direction * max_dimension
|
||||
if until == Until.NEXT
|
||||
else -direction * max_dimension
|
||||
)
|
||||
direction_axis = Axis(section.center(), clipping_direction)
|
||||
# Create a linear extrusion to start
|
||||
extrusion = Solid.extrude(section, direction * max_dimension)
|
||||
# 1: Create extrusion of length the maximum distance between profile and target
|
||||
max_dimension = find_max_dimension([profile, target])
|
||||
extrusion = Solid.extrude(profile, direction * max_dimension)
|
||||
|
||||
# Project section onto the shape to generate faces that will clip the extrusion
|
||||
# and exclude the planar faces normal to the direction of extrusion and these
|
||||
# will have no volume when extruded
|
||||
faces = []
|
||||
for face in section.project_to_shape(target_object, direction):
|
||||
if isinstance(face, Face):
|
||||
faces.append(face)
|
||||
# 2: Intersect the extrusion with the target to find the target's modified faces
|
||||
intersect_op = BRepAlgoAPI_Common(target.wrapped, extrusion.wrapped)
|
||||
intersect_op.Build()
|
||||
intersection = intersect_op.Shape()
|
||||
face_exp = TopExp_Explorer(intersection, ta.TopAbs_FACE)
|
||||
if not face_exp.More():
|
||||
raise ValueError("No intersection: extrusion does not contact target")
|
||||
|
||||
# Find the faces from the intersection that originated on the target
|
||||
history = intersect_op.History()
|
||||
modified_target_faces = []
|
||||
face_explorer = TopExp_Explorer(target.wrapped, ta.TopAbs_FACE)
|
||||
while face_explorer.More():
|
||||
target_face = TopoDS.Face_s(face_explorer.Current())
|
||||
modified_los: TopTools_ListOfShape = history.Modified(target_face)
|
||||
while not modified_los.IsEmpty():
|
||||
modified_face = TopoDS.Face_s(modified_los.First())
|
||||
modified_los.RemoveFirst()
|
||||
modified_target_faces.append(modified_face)
|
||||
face_explorer.Next()
|
||||
|
||||
# 3: Sew the resulting faces into shells - one for each surface the extrusion
|
||||
# passes through and sort by distance from the profile
|
||||
sewed_shape = _sew_topods_faces(modified_target_faces)
|
||||
|
||||
# From the sewed shape extract the shells and single faces
|
||||
top_level_shapes = get_top_level_topods_shapes(sewed_shape)
|
||||
modified_target_surfaces: ShapeList[Face | Shell] = ShapeList()
|
||||
|
||||
# For each of the top level Shells and Faces
|
||||
for top_level_shape in top_level_shapes:
|
||||
if isinstance(top_level_shape, TopoDS_Face):
|
||||
modified_target_surfaces.append(Face(top_level_shape))
|
||||
elif isinstance(top_level_shape, TopoDS_Shell):
|
||||
modified_target_surfaces.append(Shell(top_level_shape))
|
||||
else:
|
||||
faces += face.faces()
|
||||
raise RuntimeError(f"Invalid sewn shape {type(top_level_shape)}")
|
||||
|
||||
clip_faces = [
|
||||
f
|
||||
for f in faces
|
||||
if not (f.is_planar and f.normal_at().dot(direction) == 0.0)
|
||||
modified_target_surfaces = modified_target_surfaces.sort_by(
|
||||
lambda s: s.distance_to(profile)
|
||||
)
|
||||
limit = modified_target_surfaces[
|
||||
0 if until in [Until.NEXT, Until.PREVIOUS] else -1
|
||||
]
|
||||
if not clip_faces:
|
||||
raise ValueError("provided face does not intersect target_object")
|
||||
|
||||
# Create the objects that will clip the linear extrusion
|
||||
clipping_objects = [
|
||||
Solid.extrude(f, clipping_direction).fix() for f in clip_faces
|
||||
]
|
||||
clipping_objects = [o for o in clipping_objects if o.volume > 1e-9]
|
||||
|
||||
if until == Until.NEXT:
|
||||
trimmed_extrusion = extrusion.cut(target_object)
|
||||
if isinstance(trimmed_extrusion, ShapeList):
|
||||
closest_extrusion = trimmed_extrusion.sort_by(direction_axis)[0]
|
||||
else:
|
||||
closest_extrusion = trimmed_extrusion
|
||||
for clipping_object in clipping_objects:
|
||||
# It's possible for clipping faces to self intersect when they are extruded
|
||||
# thus they could be non manifold which results failed boolean operations
|
||||
# - so skip these objects
|
||||
try:
|
||||
extrusion_shapes = closest_extrusion.cut(clipping_object)
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"clipping error - extrusion may be incorrect",
|
||||
stacklevel=2,
|
||||
keep: Literal[Keep.TOP, Keep.BOTTOM] = (
|
||||
Keep.TOP if until in [Until.NEXT, Until.PREVIOUS] else Keep.BOTTOM
|
||||
)
|
||||
else:
|
||||
base_part = extrusion.intersect(target_object)
|
||||
if isinstance(base_part, ShapeList):
|
||||
extrusion_parts = base_part
|
||||
elif base_part is None:
|
||||
extrusion_parts = ShapeList()
|
||||
else:
|
||||
extrusion_parts = ShapeList([base_part])
|
||||
for clipping_object in clipping_objects:
|
||||
try:
|
||||
clipped_extrusion = extrusion.intersect(clipping_object)
|
||||
if clipped_extrusion is not None:
|
||||
extrusion_parts.append(
|
||||
clipped_extrusion.solids().sort_by(direction_axis)[0]
|
||||
)
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"clipping error - extrusion may be incorrect",
|
||||
stacklevel=2,
|
||||
)
|
||||
extrusion_shapes = Solid.fuse(*extrusion_parts)
|
||||
|
||||
result = extrusion_shapes.solids().sort_by(direction_axis)[0]
|
||||
# 4: Split the extrusion by the appropriate shell
|
||||
clipped_extrusion = extrusion.split(limit, keep=keep)
|
||||
|
||||
return result
|
||||
# 5: Return the appropriate type
|
||||
if clipped_extrusion is None:
|
||||
raise RuntimeError("Extrusion is None") # None isn't an option here
|
||||
elif isinstance(clipped_extrusion, Solid):
|
||||
return clipped_extrusion
|
||||
else:
|
||||
# isinstance(clipped_extrusion, list):
|
||||
return ShapeList(clipped_extrusion).sort_by(
|
||||
Axis(profile.center(), direction)
|
||||
)[0]
|
||||
|
||||
@classmethod
|
||||
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
||||
|
|
@ -1106,6 +1143,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Box
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeBox(
|
||||
plane.to_gp_ax2(),
|
||||
length,
|
||||
|
|
@ -1113,6 +1151,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
height,
|
||||
).Shape()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_cone(
|
||||
|
|
@ -1138,6 +1177,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial cone
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeCone(
|
||||
plane.to_gp_ax2(),
|
||||
base_radius,
|
||||
|
|
@ -1146,6 +1186,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_cylinder(
|
||||
|
|
@ -1169,6 +1210,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial cylinder
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeCylinder(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
|
|
@ -1176,6 +1218,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_loft(cls, objs: Iterable[Vertex | Wire], ruled: bool = False) -> Solid:
|
||||
|
|
@ -1195,7 +1238,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Returns:
|
||||
Solid: Lofted object
|
||||
"""
|
||||
return cls(_make_loft(objs, True, ruled))
|
||||
return cls(TopoDS.Solid_s(_make_loft(objs, True, ruled)))
|
||||
|
||||
@classmethod
|
||||
def make_sphere(
|
||||
|
|
@ -1221,6 +1264,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: sphere
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeSphere(
|
||||
plane.to_gp_ax2(),
|
||||
radius,
|
||||
|
|
@ -1229,6 +1273,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
angle3 * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_torus(
|
||||
|
|
@ -1255,6 +1300,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: Full or partial torus
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeTorus(
|
||||
plane.to_gp_ax2(),
|
||||
major_radius,
|
||||
|
|
@ -1264,6 +1310,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
major_angle * DEG2RAD,
|
||||
).Shape()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_wedge(
|
||||
|
|
@ -1293,6 +1340,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
Solid: wedge
|
||||
"""
|
||||
return cls(
|
||||
TopoDS.Solid_s(
|
||||
BRepPrimAPI_MakeWedge(
|
||||
plane.to_gp_ax2(),
|
||||
delta_x,
|
||||
|
|
@ -1304,6 +1352,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
max_z,
|
||||
).Solid()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def revolve(
|
||||
|
|
@ -1340,7 +1389,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
True,
|
||||
)
|
||||
|
||||
return cls(revol_builder.Shape())
|
||||
return cls(TopoDS.Solid_s(revol_builder.Shape()))
|
||||
|
||||
@classmethod
|
||||
def sweep(
|
||||
|
|
@ -1488,7 +1537,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
if make_solid:
|
||||
builder.MakeSolid()
|
||||
|
||||
return cls(builder.Shape())
|
||||
return cls(TopoDS.Solid_s(builder.Shape()))
|
||||
|
||||
@classmethod
|
||||
def thicken(
|
||||
|
|
@ -1544,7 +1593,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
)
|
||||
offset_builder.MakeOffsetShape()
|
||||
try:
|
||||
result = Solid(offset_builder.Shape())
|
||||
result = Solid(TopoDS.Solid_s(offset_builder.Shape()))
|
||||
except StdFail_NotDone as err:
|
||||
raise RuntimeError("Error applying thicken to given surface") from err
|
||||
|
||||
|
|
@ -1591,7 +1640,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
|||
|
||||
try:
|
||||
draft_angle_builder.Build()
|
||||
result = Solid(draft_angle_builder.Shape())
|
||||
result = Solid(TopoDS.Solid_s(draft_angle_builder.Shape()))
|
||||
except StdFail_NotDone as err:
|
||||
raise DraftAngleError(
|
||||
"Draft build failed on the given solid.",
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ from .shape_core import (
|
|||
ShapeList,
|
||||
SkipClean,
|
||||
_sew_topods_faces,
|
||||
_topods_bool_op,
|
||||
_topods_entities,
|
||||
_topods_face_normal_at,
|
||||
downcast,
|
||||
|
|
@ -155,7 +156,6 @@ from .utils import (
|
|||
_extrude_topods_shape,
|
||||
_make_loft,
|
||||
_make_topods_face_from_wires,
|
||||
_topods_bool_op,
|
||||
find_max_dimension,
|
||||
)
|
||||
from .zero_d import Vertex
|
||||
|
|
@ -171,7 +171,6 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
|||
"""Additional methods to add to Face and Shell class"""
|
||||
|
||||
project_to_viewport = Mixin1D.project_to_viewport
|
||||
split = Mixin1D.split
|
||||
|
||||
vertices = Mixin1D.vertices
|
||||
vertex = Mixin1D.vertex
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ Key Features:
|
|||
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
||||
|
||||
- **Boolean Operations**:
|
||||
- `_topods_bool_op`: Generic Boolean operations for TopoDS_Shapes.
|
||||
- `new_edges`: Identifies newly created edges from combined shapes.
|
||||
|
||||
- **Enhanced Math**:
|
||||
|
|
@ -282,45 +281,6 @@ def _make_topods_face_from_wires(
|
|||
return TopoDS.Face_s(sf_f.Result())
|
||||
|
||||
|
||||
def _topods_bool_op(
|
||||
args: Iterable[TopoDS_Shape],
|
||||
tools: Iterable[TopoDS_Shape],
|
||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
|
||||
) -> TopoDS_Shape:
|
||||
"""Generic boolean operation for TopoDS_Shapes
|
||||
|
||||
Args:
|
||||
args: Iterable[TopoDS_Shape]:
|
||||
tools: Iterable[TopoDS_Shape]:
|
||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
|
||||
|
||||
Returns: TopoDS_Shape
|
||||
|
||||
"""
|
||||
args = list(args)
|
||||
tools = list(tools)
|
||||
arg = TopTools_ListOfShape()
|
||||
for obj in args:
|
||||
arg.Append(obj)
|
||||
|
||||
tool = TopTools_ListOfShape()
|
||||
for obj in tools:
|
||||
tool.Append(obj)
|
||||
|
||||
operation.SetArguments(arg)
|
||||
operation.SetTools(tool)
|
||||
|
||||
operation.SetRunParallel(True)
|
||||
operation.Build()
|
||||
|
||||
result = downcast(operation.Shape())
|
||||
# Remove unnecessary TopoDS_Compound around single shape
|
||||
if isinstance(result, TopoDS_Compound):
|
||||
result = unwrap_topods_compound(result, True)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]:
|
||||
"""Compare the OCCT objects of each list and return the differences"""
|
||||
shapes_one = list(shapes_one)
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ from OCP.TopExp import TopExp_Explorer
|
|||
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
|
||||
from OCP.gp import gp_Pnt
|
||||
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
||||
|
||||
from .shape_core import Shape, ShapeList, downcast, shapetype
|
||||
from build123d.build_enums import Keep
|
||||
from .shape_core import Shape, ShapeList, TrimmingTool, downcast, shapetype
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
|
|
@ -161,7 +161,7 @@ class Vertex(Shape[TopoDS_Vertex]):
|
|||
|
||||
shape_type = shapetype(obj)
|
||||
# NB downcast is needed to handle TopoDS_Shape types
|
||||
return constructor_lut[shape_type](downcast(obj))
|
||||
return constructor_lut[shape_type](TopoDS.Vertex_s(obj))
|
||||
|
||||
@classmethod
|
||||
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
||||
|
|
@ -312,6 +312,10 @@ class Vertex(Shape[TopoDS_Vertex]):
|
|||
"""The center of a vertex is itself!"""
|
||||
return Vector(self)
|
||||
|
||||
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
||||
"""split - not implemented"""
|
||||
raise NotImplementedError("Vertices cannot be split.")
|
||||
|
||||
def to_tuple(self) -> tuple[float, float, float]:
|
||||
"""Return vertex as three tuple of floats"""
|
||||
warnings.warn(
|
||||
|
|
|
|||
|
|
@ -330,6 +330,60 @@ class TestExtrude(unittest.TestCase):
|
|||
extrude(until=Until.NEXT)
|
||||
self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5)
|
||||
|
||||
def test_extrude_until2(self):
|
||||
target = Box(10, 5, 5) - Pos(X=2.5) * Cylinder(0.5, 5)
|
||||
pln = Plane((7, 0, 7), z_dir=(-1, 0, -1))
|
||||
profile = (pln * Circle(1)).face()
|
||||
extrusion = extrude(profile, dir=pln.z_dir, until=Until.NEXT, target=target)
|
||||
self.assertLess(extrusion.bounding_box().min.Z, 2.5)
|
||||
|
||||
def test_extrude_until3(self):
|
||||
with BuildPart() as p:
|
||||
with BuildSketch(Plane.XZ):
|
||||
Rectangle(8, 8, align=Align.MIN)
|
||||
with Locations((1, 1)):
|
||||
Rectangle(7, 7, align=Align.MIN, mode=Mode.SUBTRACT)
|
||||
extrude(amount=2, both=True)
|
||||
with BuildSketch(
|
||||
Plane((-2, 0, -2), x_dir=(0, 1, 0), z_dir=(1, 0, 1))
|
||||
) as profile:
|
||||
Rectangle(4, 1)
|
||||
extrude(until=Until.NEXT)
|
||||
|
||||
self.assertAlmostEqual(p.part.volume, 72.313, 2)
|
||||
|
||||
def test_extrude_until_errors(self):
|
||||
with self.assertRaises(ValueError):
|
||||
extrude(
|
||||
Rectangle(1, 1),
|
||||
until=Until.NEXT,
|
||||
dir=(0, 0, 1),
|
||||
target=Pos(Z=-10) * Box(1, 1, 1),
|
||||
)
|
||||
|
||||
def test_extrude_until_invalid_sewn_shape(self):
|
||||
profile = Face.make_rect(1, 1)
|
||||
target = Box(2, 2, 2)
|
||||
direction = Vector(0, 0, 1)
|
||||
|
||||
bad_shape = Box(1, 1, 1).wrapped # not a Face or Shell → forces RuntimeError
|
||||
|
||||
with patch(
|
||||
"build123d.topology.three_d.get_top_level_topods_shapes",
|
||||
return_value=[bad_shape],
|
||||
):
|
||||
with self.assertRaises(RuntimeError):
|
||||
extrude(profile, dir=direction, until=Until.NEXT, target=target)
|
||||
|
||||
def test_extrude_until_invalid_split(self):
|
||||
profile = Face.make_rect(1, 1)
|
||||
target = Box(2, 2, 2)
|
||||
direction = Vector(0, 0, 1)
|
||||
|
||||
with patch("build123d.topology.three_d.Solid.split", return_value=None):
|
||||
with self.assertRaises(RuntimeError):
|
||||
extrude(profile, dir=direction, until=Until.NEXT, target=target)
|
||||
|
||||
def test_extrude_face(self):
|
||||
with BuildPart(Plane.XZ) as box:
|
||||
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue