mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -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,16 +47,17 @@ with BuildPart() as ppp109:
|
||||||
split(bisect_by=Plane.YZ)
|
split(bisect_by=Plane.YZ)
|
||||||
extrude(amount=6)
|
extrude(amount=6)
|
||||||
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
||||||
# extrude(f, until=Until.NEXT) # throws a warning
|
extrude(f, until=Until.NEXT)
|
||||||
extrude(f, amount=10)
|
fillet(ppp109.edges().filter_by(Axis.Y).sort_by(Axis.Z)[2], 16)
|
||||||
fillet(ppp109.edge(Select.NEW), 16)
|
# extrude(f, amount=10)
|
||||||
|
# fillet(ppp109.edges(Select.NEW), 16)
|
||||||
|
|
||||||
|
|
||||||
show(ppp109)
|
show(ppp109)
|
||||||
|
|
||||||
got_mass = ppp109.part.volume*densb
|
got_mass = ppp109.part.volume * densb
|
||||||
want_mass = 307.23
|
want_mass = 307.23
|
||||||
tolerance = 1
|
tolerance = 1
|
||||||
delta = abs(got_mass - want_mass)
|
delta = abs(got_mass - want_mass)
|
||||||
print(f"Mass: {got_mass:0.2f} g")
|
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(
|
new_solids.append(
|
||||||
Solid.extrude_until(
|
Solid.extrude_until(
|
||||||
section=face,
|
face,
|
||||||
target_object=target_object,
|
target=target_object,
|
||||||
direction=plane.z_dir * direction,
|
direction=plane.z_dir * direction,
|
||||||
until=until,
|
until=until,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -233,11 +233,11 @@ from .shape_core import (
|
||||||
shapetype,
|
shapetype,
|
||||||
topods_dim,
|
topods_dim,
|
||||||
unwrap_topods_compound,
|
unwrap_topods_compound,
|
||||||
|
_topods_bool_op,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
_make_topods_face_from_wires,
|
_make_topods_face_from_wires,
|
||||||
_topods_bool_op,
|
|
||||||
isclose_b,
|
isclose_b,
|
||||||
)
|
)
|
||||||
from .zero_d import Vertex, topo_explore_common_vertex
|
from .zero_d import Vertex, topo_explore_common_vertex
|
||||||
|
|
@ -1377,144 +1377,6 @@ class Mixin1D(Shape[TOPODS]):
|
||||||
|
|
||||||
return (visible_edges, hidden_edges)
|
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:
|
def start_point(self) -> Vector:
|
||||||
"""The start point of this edge
|
"""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.BRepGProp import BRepGProp, BRepGProp_Face
|
||||||
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||||
|
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||||
from OCP.BRepTools import BRepTools
|
from OCP.BRepTools import BRepTools
|
||||||
from OCP.gce import gce_MakeLin
|
from OCP.gce import gce_MakeLin
|
||||||
from OCP.Geom import Geom_Line
|
from OCP.Geom import Geom_Line
|
||||||
|
|
@ -196,7 +197,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
ta.TopAbs_COMPSOLID: "CompSolid",
|
ta.TopAbs_COMPSOLID: "CompSolid",
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_properties_LUT = {
|
shape_properties_LUT: dict[TopAbs_ShapeEnum:function] = {
|
||||||
ta.TopAbs_VERTEX: None,
|
ta.TopAbs_VERTEX: None,
|
||||||
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
||||||
ta.TopAbs_WIRE: 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"""
|
"""solids - all the solids in this Shape"""
|
||||||
return ShapeList()
|
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
|
@overload
|
||||||
def split_by_perimeter(
|
def split_by_perimeter(
|
||||||
self, perimeter: Edge | Wire, keep: Literal[Keep.INSIDE, Keep.OUTSIDE]
|
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())
|
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]:
|
def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||||
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
||||||
out = {} # using dict to prevent duplicates
|
out = {} # using dict to prevent duplicates
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,10 @@ license:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
|
||||||
import warnings
|
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from math import radians, cos, tan
|
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
|
from typing_extensions import Self
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
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.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
|
||||||
from OCP.LocOpe import LocOpe_DPrism
|
from OCP.LocOpe import LocOpe_DPrism
|
||||||
from OCP.ShapeFix import ShapeFix_Solid
|
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.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.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 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 (
|
from build123d.geometry import (
|
||||||
DEG2RAD,
|
DEG2RAD,
|
||||||
TOLERANCE,
|
TOLERANCE,
|
||||||
|
|
@ -107,7 +113,19 @@ from build123d.geometry import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .one_d import Edge, Wire, Mixin1D
|
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 .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
||||||
from .utils import (
|
from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
|
|
@ -126,7 +144,6 @@ class Mixin3D(Shape[TOPODS]):
|
||||||
"""Additional methods to add to 3D Shape classes"""
|
"""Additional methods to add to 3D Shape classes"""
|
||||||
|
|
||||||
project_to_viewport = Mixin1D.project_to_viewport
|
project_to_viewport = Mixin1D.project_to_viewport
|
||||||
split = Mixin1D.split
|
|
||||||
find_intersection_points = Mixin2D.find_intersection_points
|
find_intersection_points = Mixin2D.find_intersection_points
|
||||||
|
|
||||||
vertices = Mixin1D.vertices
|
vertices = Mixin1D.vertices
|
||||||
|
|
@ -507,7 +524,9 @@ class Mixin3D(Shape[TOPODS]):
|
||||||
for obj in common_set:
|
for obj in common_set:
|
||||||
match (obj, target):
|
match (obj, target):
|
||||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
|
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
|
||||||
operation = BRepAlgoAPI_Section()
|
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common = (
|
||||||
|
BRepAlgoAPI_Section()
|
||||||
|
)
|
||||||
result = bool_op((obj,), (target,), operation)
|
result = bool_op((obj,), (target,), operation)
|
||||||
if (
|
if (
|
||||||
not isinstance(obj, Edge | Wire)
|
not isinstance(obj, Edge | Wire)
|
||||||
|
|
@ -610,8 +629,10 @@ class Mixin3D(Shape[TOPODS]):
|
||||||
try:
|
try:
|
||||||
new_shape = self.__class__(fillet_builder.Shape())
|
new_shape = self.__class__(fillet_builder.Shape())
|
||||||
if not new_shape.is_valid:
|
if not new_shape.is_valid:
|
||||||
raise fillet_exception
|
# raise fillet_exception
|
||||||
except fillet_exception:
|
raise Standard_Failure
|
||||||
|
# except fillet_exception:
|
||||||
|
except (Standard_Failure, StdFail_NotDone):
|
||||||
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
||||||
|
|
||||||
# These numbers work, are they close enough? - if not try larger window
|
# 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
|
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
|
||||||
# specific exceptions are required.
|
# specific exceptions are required.
|
||||||
if platform.system() == "Darwin":
|
# if platform.system() == "Darwin":
|
||||||
fillet_exception = Standard_Failure
|
# fillet_exception = Standard_Failure
|
||||||
else:
|
# else:
|
||||||
fillet_exception = StdFail_NotDone
|
# fillet_exception = StdFail_NotDone
|
||||||
|
|
||||||
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
|
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)
|
inner_comp = _make_topods_compound_from_shapes(inner_solids)
|
||||||
|
|
||||||
# subtract from the outer solid
|
# 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
|
@classmethod
|
||||||
def extrude_taper(
|
def extrude_taper(
|
||||||
|
|
@ -933,7 +964,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
direction.length / cos(radians(taper)),
|
direction.length / cos(radians(taper)),
|
||||||
radians(taper),
|
radians(taper),
|
||||||
)
|
)
|
||||||
new_solid = Solid(prism_builder.Shape())
|
new_solid = Solid(TopoDS.Solid_s(prism_builder.Shape()))
|
||||||
else:
|
else:
|
||||||
# Determine the offset to get the taper
|
# Determine the offset to get the taper
|
||||||
offset_amt = -direction.length * tan(radians(taper))
|
offset_amt = -direction.length * tan(radians(taper))
|
||||||
|
|
@ -972,110 +1003,116 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def extrude_until(
|
def extrude_until(
|
||||||
cls,
|
cls,
|
||||||
section: Face,
|
profile: Face,
|
||||||
target_object: Compound | Solid,
|
target: Compound | Solid,
|
||||||
direction: VectorLike,
|
direction: VectorLike,
|
||||||
until: Until = Until.NEXT,
|
until: Until = Until.NEXT,
|
||||||
) -> Compound | Solid:
|
) -> Solid:
|
||||||
"""extrude_until
|
"""extrude_until
|
||||||
|
|
||||||
Extrude section in provided direction until it encounters either the
|
Extrude `profile` in the provided `direction` until it encounters a
|
||||||
NEXT or LAST surface of target_object. Note that the bounding surface
|
bounding surface on the `target`. The termination surface is chosen
|
||||||
must be larger than the extruded face where they contact.
|
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:
|
Args:
|
||||||
section (Face): Face to extrude
|
profile (Face): The face to extrude.
|
||||||
target_object (Union[Compound, Solid]): object to limit extrusion
|
target (Union[Compound, Solid]): The object that limits the extrusion.
|
||||||
direction (VectorLike): extrusion direction
|
direction (VectorLike): Extrusion direction.
|
||||||
until (Until, optional): surface to limit extrusion. Defaults to Until.NEXT.
|
until (Until, optional): Surface selection mode controlling which
|
||||||
|
intersection to stop at. Defaults to ``Until.NEXT``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: provided face does not intersect target_object
|
ValueError: If the provided profile does not intersect the target.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Union[Compound, Solid]: extruded Face
|
Solid: The extruded and limited solid.
|
||||||
"""
|
"""
|
||||||
direction = Vector(direction)
|
direction = Vector(direction)
|
||||||
if until in [Until.PREVIOUS, Until.FIRST]:
|
if until in [Until.PREVIOUS, Until.FIRST]:
|
||||||
direction *= -1
|
direction *= -1
|
||||||
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
||||||
|
|
||||||
max_dimension = find_max_dimension([section, target_object])
|
# 1: Create extrusion of length the maximum distance between profile and target
|
||||||
clipping_direction = (
|
max_dimension = find_max_dimension([profile, target])
|
||||||
direction * max_dimension
|
extrusion = Solid.extrude(profile, 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)
|
|
||||||
|
|
||||||
# Project section onto the shape to generate faces that will clip the extrusion
|
# 2: Intersect the extrusion with the target to find the target's modified faces
|
||||||
# and exclude the planar faces normal to the direction of extrusion and these
|
intersect_op = BRepAlgoAPI_Common(target.wrapped, extrusion.wrapped)
|
||||||
# will have no volume when extruded
|
intersect_op.Build()
|
||||||
faces = []
|
intersection = intersect_op.Shape()
|
||||||
for face in section.project_to_shape(target_object, direction):
|
face_exp = TopExp_Explorer(intersection, ta.TopAbs_FACE)
|
||||||
if isinstance(face, Face):
|
if not face_exp.More():
|
||||||
faces.append(face)
|
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:
|
else:
|
||||||
faces += face.faces()
|
raise RuntimeError(f"Invalid sewn shape {type(top_level_shape)}")
|
||||||
|
|
||||||
clip_faces = [
|
modified_target_surfaces = modified_target_surfaces.sort_by(
|
||||||
f
|
lambda s: s.distance_to(profile)
|
||||||
for f in faces
|
)
|
||||||
if not (f.is_planar and f.normal_at().dot(direction) == 0.0)
|
limit = modified_target_surfaces[
|
||||||
|
0 if until in [Until.NEXT, Until.PREVIOUS] else -1
|
||||||
]
|
]
|
||||||
if not clip_faces:
|
keep: Literal[Keep.TOP, Keep.BOTTOM] = (
|
||||||
raise ValueError("provided face does not intersect target_object")
|
Keep.TOP if until in [Until.NEXT, Until.PREVIOUS] else Keep.BOTTOM
|
||||||
|
|
||||||
# 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,
|
|
||||||
)
|
)
|
||||||
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
|
@classmethod
|
||||||
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
||||||
|
|
@ -1106,6 +1143,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: Box
|
Solid: Box
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeBox(
|
BRepPrimAPI_MakeBox(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
length,
|
length,
|
||||||
|
|
@ -1113,6 +1151,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
height,
|
height,
|
||||||
).Shape()
|
).Shape()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_cone(
|
def make_cone(
|
||||||
|
|
@ -1138,6 +1177,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: Full or partial cone
|
Solid: Full or partial cone
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeCone(
|
BRepPrimAPI_MakeCone(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
base_radius,
|
base_radius,
|
||||||
|
|
@ -1146,6 +1186,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
angle * DEG2RAD,
|
angle * DEG2RAD,
|
||||||
).Shape()
|
).Shape()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_cylinder(
|
def make_cylinder(
|
||||||
|
|
@ -1169,6 +1210,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: Full or partial cylinder
|
Solid: Full or partial cylinder
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeCylinder(
|
BRepPrimAPI_MakeCylinder(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
radius,
|
radius,
|
||||||
|
|
@ -1176,6 +1218,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
angle * DEG2RAD,
|
angle * DEG2RAD,
|
||||||
).Shape()
|
).Shape()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_loft(cls, objs: Iterable[Vertex | Wire], ruled: bool = False) -> Solid:
|
def make_loft(cls, objs: Iterable[Vertex | Wire], ruled: bool = False) -> Solid:
|
||||||
|
|
@ -1195,7 +1238,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Returns:
|
Returns:
|
||||||
Solid: Lofted object
|
Solid: Lofted object
|
||||||
"""
|
"""
|
||||||
return cls(_make_loft(objs, True, ruled))
|
return cls(TopoDS.Solid_s(_make_loft(objs, True, ruled)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_sphere(
|
def make_sphere(
|
||||||
|
|
@ -1221,6 +1264,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: sphere
|
Solid: sphere
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeSphere(
|
BRepPrimAPI_MakeSphere(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
radius,
|
radius,
|
||||||
|
|
@ -1229,6 +1273,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
angle3 * DEG2RAD,
|
angle3 * DEG2RAD,
|
||||||
).Shape()
|
).Shape()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_torus(
|
def make_torus(
|
||||||
|
|
@ -1255,6 +1300,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: Full or partial torus
|
Solid: Full or partial torus
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeTorus(
|
BRepPrimAPI_MakeTorus(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
major_radius,
|
major_radius,
|
||||||
|
|
@ -1264,6 +1310,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
major_angle * DEG2RAD,
|
major_angle * DEG2RAD,
|
||||||
).Shape()
|
).Shape()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_wedge(
|
def make_wedge(
|
||||||
|
|
@ -1293,6 +1340,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
Solid: wedge
|
Solid: wedge
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
|
TopoDS.Solid_s(
|
||||||
BRepPrimAPI_MakeWedge(
|
BRepPrimAPI_MakeWedge(
|
||||||
plane.to_gp_ax2(),
|
plane.to_gp_ax2(),
|
||||||
delta_x,
|
delta_x,
|
||||||
|
|
@ -1304,6 +1352,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
max_z,
|
max_z,
|
||||||
).Solid()
|
).Solid()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revolve(
|
def revolve(
|
||||||
|
|
@ -1340,7 +1389,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(revol_builder.Shape())
|
return cls(TopoDS.Solid_s(revol_builder.Shape()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sweep(
|
def sweep(
|
||||||
|
|
@ -1488,7 +1537,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
if make_solid:
|
if make_solid:
|
||||||
builder.MakeSolid()
|
builder.MakeSolid()
|
||||||
|
|
||||||
return cls(builder.Shape())
|
return cls(TopoDS.Solid_s(builder.Shape()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def thicken(
|
def thicken(
|
||||||
|
|
@ -1544,7 +1593,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
)
|
)
|
||||||
offset_builder.MakeOffsetShape()
|
offset_builder.MakeOffsetShape()
|
||||||
try:
|
try:
|
||||||
result = Solid(offset_builder.Shape())
|
result = Solid(TopoDS.Solid_s(offset_builder.Shape()))
|
||||||
except StdFail_NotDone as err:
|
except StdFail_NotDone as err:
|
||||||
raise RuntimeError("Error applying thicken to given surface") from err
|
raise RuntimeError("Error applying thicken to given surface") from err
|
||||||
|
|
||||||
|
|
@ -1591,7 +1640,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
draft_angle_builder.Build()
|
draft_angle_builder.Build()
|
||||||
result = Solid(draft_angle_builder.Shape())
|
result = Solid(TopoDS.Solid_s(draft_angle_builder.Shape()))
|
||||||
except StdFail_NotDone as err:
|
except StdFail_NotDone as err:
|
||||||
raise DraftAngleError(
|
raise DraftAngleError(
|
||||||
"Draft build failed on the given solid.",
|
"Draft build failed on the given solid.",
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ from .shape_core import (
|
||||||
ShapeList,
|
ShapeList,
|
||||||
SkipClean,
|
SkipClean,
|
||||||
_sew_topods_faces,
|
_sew_topods_faces,
|
||||||
|
_topods_bool_op,
|
||||||
_topods_entities,
|
_topods_entities,
|
||||||
_topods_face_normal_at,
|
_topods_face_normal_at,
|
||||||
downcast,
|
downcast,
|
||||||
|
|
@ -155,7 +156,6 @@ from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
_make_loft,
|
_make_loft,
|
||||||
_make_topods_face_from_wires,
|
_make_topods_face_from_wires,
|
||||||
_topods_bool_op,
|
|
||||||
find_max_dimension,
|
find_max_dimension,
|
||||||
)
|
)
|
||||||
from .zero_d import Vertex
|
from .zero_d import Vertex
|
||||||
|
|
@ -171,7 +171,6 @@ class Mixin2D(ABC, Shape[TOPODS]):
|
||||||
"""Additional methods to add to Face and Shell class"""
|
"""Additional methods to add to Face and Shell class"""
|
||||||
|
|
||||||
project_to_viewport = Mixin1D.project_to_viewport
|
project_to_viewport = Mixin1D.project_to_viewport
|
||||||
split = Mixin1D.split
|
|
||||||
|
|
||||||
vertices = Mixin1D.vertices
|
vertices = Mixin1D.vertices
|
||||||
vertex = Mixin1D.vertex
|
vertex = Mixin1D.vertex
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ Key Features:
|
||||||
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
||||||
|
|
||||||
- **Boolean Operations**:
|
- **Boolean Operations**:
|
||||||
- `_topods_bool_op`: Generic Boolean operations for TopoDS_Shapes.
|
|
||||||
- `new_edges`: Identifies newly created edges from combined shapes.
|
- `new_edges`: Identifies newly created edges from combined shapes.
|
||||||
|
|
||||||
- **Enhanced Math**:
|
- **Enhanced Math**:
|
||||||
|
|
@ -282,45 +281,6 @@ def _make_topods_face_from_wires(
|
||||||
return TopoDS.Face_s(sf_f.Result())
|
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]:
|
def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]:
|
||||||
"""Compare the OCCT objects of each list and return the differences"""
|
"""Compare the OCCT objects of each list and return the differences"""
|
||||||
shapes_one = list(shapes_one)
|
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.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
|
||||||
from OCP.gp import gp_Pnt
|
from OCP.gp import gp_Pnt
|
||||||
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
||||||
|
from build123d.build_enums import Keep
|
||||||
from .shape_core import Shape, ShapeList, downcast, shapetype
|
from .shape_core import Shape, ShapeList, TrimmingTool, downcast, shapetype
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
|
@ -161,7 +161,7 @@ class Vertex(Shape[TopoDS_Vertex]):
|
||||||
|
|
||||||
shape_type = shapetype(obj)
|
shape_type = shapetype(obj)
|
||||||
# NB downcast is needed to handle TopoDS_Shape types
|
# 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
|
@classmethod
|
||||||
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
||||||
|
|
@ -312,6 +312,10 @@ class Vertex(Shape[TopoDS_Vertex]):
|
||||||
"""The center of a vertex is itself!"""
|
"""The center of a vertex is itself!"""
|
||||||
return Vector(self)
|
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]:
|
def to_tuple(self) -> tuple[float, float, float]:
|
||||||
"""Return vertex as three tuple of floats"""
|
"""Return vertex as three tuple of floats"""
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,60 @@ class TestExtrude(unittest.TestCase):
|
||||||
extrude(until=Until.NEXT)
|
extrude(until=Until.NEXT)
|
||||||
self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5)
|
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):
|
def test_extrude_face(self):
|
||||||
with BuildPart(Plane.XZ) as box:
|
with BuildPart(Plane.XZ) as box:
|
||||||
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue