mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-05 18:20:46 -08:00
Merge branch 'dev' into lexer
This commit is contained in:
commit
caa25671fb
29 changed files with 1042 additions and 350 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -41,3 +41,6 @@ venv.bak/
|
|||
|
||||
# Profiling debris.
|
||||
prof/
|
||||
|
||||
# MacOS cruft
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ tests, ensure they build and pass, and ensure that `pylint` and `mypy`
|
|||
are happy with your code.
|
||||
|
||||
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
|
||||
- Install development dependencies: `pip install -e .[development]`
|
||||
- Install docs dependencies: `pip install -e .[docs]`
|
||||
- Install development dependencies: `pip install -e ".[development]"`
|
||||
- Install docs dependencies: `pip install -e ".[docs]"`
|
||||
- Install `build123d` in editable mode from current dir: `pip install -e .`
|
||||
- Run tests with: `python -m pytest -n auto`
|
||||
- Build docs with: `cd docs && make html`
|
||||
|
|
|
|||
15
NOTICE
Normal file
15
NOTICE
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
build123d
|
||||
Copyright (c) 2022–2025 The build123d Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
This project was originally derived from portions of the CadQuery codebase
|
||||
(https://github.com/CadQuery/cadquery) but has since been extensively
|
||||
refactored and restructured into an independent system.
|
||||
CadQuery is licensed under the Apache License, Version 2.0.
|
||||
18
README.md
18
README.md
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img alt="build123d logo" src="docs/assets/build123d_logo/logo-banner.svg">
|
||||
<img alt="build123d logo" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/build123d_logo/logo-banner.svg">
|
||||
</p>
|
||||
|
||||
[](https://build123d.readthedocs.io/en/latest/?badge=latest)
|
||||
|
|
@ -19,9 +19,17 @@
|
|||
[](https://doi.org/10.5281/zenodo.14872322)
|
||||
|
||||
|
||||
Build123d is a python-based, parametric, [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. It's built on the [Open Cascade] geometric kernel and allows for the creation of complex models using a simple and intuitive python syntax. Build123d can be used to create models for 3D printing, CNC machining, laser cutting, and other manufacturing processes. Models can be exported to a wide variety of popular CAD tools such as [FreeCAD] and SolidWorks.
|
||||
Build123d is a Python-based, parametric [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. Built on the [Open Cascade] geometric kernel, it provides a clean, fully Pythonic interface for creating precise models suitable for 3D printing, CNC machining, laser cutting, and other manufacturing processes. Models can be exported to popular CAD tools such as [FreeCAD] and SolidWorks.
|
||||
|
||||
Build123d could be considered as an evolution of [CadQuery] where the somewhat restrictive Fluent API (method chaining) is replaced with stateful context managers - e.g. `with` blocks - thus enabling the full python toolbox: for loops, references to objects, object sorting and filtering, etc.
|
||||
Designed for modern, maintainable CAD-as-code, build123d combines clear architecture with expressive, algebraic modeling. It offers:
|
||||
- Minimal or no internal state depending on mode,
|
||||
- Explicit 1D, 2D, and 3D geometry classes with well-defined operations,
|
||||
- Extensibility through subclassing and functional composition—no monkey patching,
|
||||
- Standards-compliant code (PEP 8, mypy, pylint) with rich pylance type hints,
|
||||
- Deep Python integration—selectors as lists, locations as iterables, and natural conversions (Solid(shell), tuple(Vector)),
|
||||
- Operator-driven modeling (obj += sub_obj, Plane.XZ * Pos(X=5) * Rectangle(1, 1)) for algebraic, readable, and composable design logic.
|
||||
|
||||
The result is a framework that feels native to Python while providing the full power of OpenCascade geometry underneath.
|
||||
|
||||
The documentation for **build123d** can be found at [readthedocs](https://build123d.readthedocs.io/en/latest/index.html).
|
||||
|
||||
|
|
@ -62,6 +70,10 @@ python3 -m pip install -e .
|
|||
|
||||
Further installation instructions are available (e.g. Poetry) see the [installation section on readthedocs](https://build123d.readthedocs.io/en/latest/installation.html).
|
||||
|
||||
Attribution:
|
||||
|
||||
Build123d was originally derived from portions of the [CadQuery] codebase but has since been extensively refactored and restructured into an independent system.
|
||||
|
||||
[BREP]: https://en.wikipedia.org/wiki/Boundary_representation
|
||||
[CadQuery]: https://cadquery.readthedocs.io/en/latest/index.html
|
||||
[FreeCAD]: https://www.freecad.org/
|
||||
|
|
|
|||
|
|
@ -29,58 +29,35 @@
|
|||
:align: center
|
||||
:alt: build123d logo
|
||||
|
||||
Build123d is a python-based, parametric, boundary representation (BREP) modeling
|
||||
framework for 2D and 3D CAD. It's built on the Open Cascade geometric kernel and
|
||||
allows for the creation of complex models using a simple and intuitive python
|
||||
syntax. Build123d can be used to create models for 3D printing, CNC machining,
|
||||
laser cutting, and other manufacturing processes. Models can be exported to a
|
||||
wide variety of popular CAD tools such as FreeCAD and SolidWorks.
|
||||
|
||||
Build123d could be considered as an evolution of
|
||||
`CadQuery <https://cadquery.readthedocs.io/en/latest/index.html>`_ where the
|
||||
somewhat restrictive Fluent API (method chaining) is replaced with stateful
|
||||
context managers - i.e. `with` blocks - thus enabling the full python toolbox:
|
||||
for loops, references to objects, object sorting and filtering, etc.
|
||||
|
||||
Note that this documentation is available in
|
||||
`pdf <https://build123d.readthedocs.io/_/downloads/en/latest/pdf/>`_ and
|
||||
`epub <https://build123d.readthedocs.io/_/downloads/en/latest/epub/>`_ formats
|
||||
for reference while offline.
|
||||
|
||||
########
|
||||
Overview
|
||||
About
|
||||
########
|
||||
|
||||
build123d uses the standard python context manager - i.e. the ``with`` statement often used when
|
||||
working with files - as a builder of the object under construction. Once the object is complete
|
||||
it can be extracted from the builders and used in other ways: for example exported as a STEP
|
||||
file or used in an Assembly. There are three builders available:
|
||||
Build123d is a Python-based, parametric (BREP) modeling framework for 2D and 3D CAD.
|
||||
Built on the Open Cascade geometric kernel, it provides a clean, fully Pythonic interface
|
||||
for creating precise models suitable for 3D printing, CNC machining, laser cutting, and
|
||||
other manufacturing processes. Models can be exported to popular CAD tools such as FreeCAD
|
||||
and SolidWorks.
|
||||
|
||||
* **BuildLine**: a builder of one dimensional objects - those with the property
|
||||
of length but not of area or volume - typically used
|
||||
to create complex lines used in sketches or paths.
|
||||
* **BuildSketch**: a builder of planar two dimensional objects - those with the property
|
||||
of area but not of volume - typically used to create 2D drawings that are extruded into 3D parts.
|
||||
* **BuildPart**: a builder of three dimensional objects - those with the property of volume -
|
||||
used to create individual parts.
|
||||
Designed for modern, maintainable CAD-as-code, build123d combines clear architecture with
|
||||
expressive, algebraic modeling. It offers:
|
||||
|
||||
The three builders work together in a hierarchy as follows:
|
||||
* Minimal or no internal state depending on mode
|
||||
* Explicit 1D, 2D, and 3D geometry classes with well-defined operations
|
||||
* Extensibility through subclassing and functional composition—no monkey patching
|
||||
* Standards-compliant code (PEP 8, mypy, pylint) with rich pylance type hints
|
||||
* Deep Python integration—selectors as lists, locations as iterables, and natural
|
||||
conversions (``Solid(shell)``, ``tuple(Vector)``)
|
||||
* Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``)
|
||||
for algebraic, readable, and composable design logic
|
||||
|
||||
.. code-block:: build123d
|
||||
|
||||
with BuildPart() as my_part:
|
||||
...
|
||||
with BuildSketch() as my_sketch:
|
||||
...
|
||||
with BuildLine() as my_line:
|
||||
...
|
||||
...
|
||||
...
|
||||
|
||||
where ``my_line`` will be added to ``my_sketch`` once the line is complete and ``my_sketch`` will be
|
||||
added to ``my_part`` once the sketch is complete.
|
||||
With build123d, intricate parametric models can be created in just a few lines of readable
|
||||
Python code—as demonstrated by the tea cup example below.
|
||||
|
||||
As an example, consider the design of a tea cup:
|
||||
.. dropdown:: Teacup Example
|
||||
|
||||
.. literalinclude:: ../examples/tea_cup.py
|
||||
:language: build123d
|
||||
|
|
@ -92,6 +69,14 @@ As an example, consider the design of a tea cup:
|
|||
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
|
||||
<model-viewer poster="_images/tea_cup.png" src="_static/tea_cup.glb" alt="A tea cup modelled in build123d" auto-rotate camera-controls style="width: 100%; height: 50vh;"></model-viewer>
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
This documentation is available in
|
||||
`pdf <https://build123d.readthedocs.io/_/downloads/en/latest/pdf/>`_ and
|
||||
`epub <https://build123d.readthedocs.io/_/downloads/en/latest/epub/>`_ formats
|
||||
for reference while offline.
|
||||
|
||||
.. note::
|
||||
|
||||
There is a `Discord <https://discord.com/invite/Bj9AQPsCfx>`_ server (shared with CadQuery) where
|
||||
|
|
|
|||
|
|
@ -68,8 +68,7 @@ with BuildPart() as ex26:
|
|||
with BuildSketch() as ex26_sk:
|
||||
with Locations((0, rev)):
|
||||
Circle(rad)
|
||||
revolve(axis=Axis.X, revolution_arc=90)
|
||||
mirror(about=Plane.XZ)
|
||||
revolve(axis=Axis.X, revolution_arc=180)
|
||||
with BuildSketch() as ex26_sk2:
|
||||
Rectangle(rad, rev)
|
||||
ex26_target = ex26.part
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ rad, rev = 3, 25
|
|||
|
||||
# Extrude last
|
||||
circle = Pos(0, rev) * Circle(rad)
|
||||
ex26_target = revolve(circle, Axis.X, revolution_arc=90)
|
||||
ex26_target = ex26_target + mirror(ex26_target, Plane.XZ)
|
||||
ex26_target = revolve(circle, Axis.X, revolution_arc=180)
|
||||
ex26_target = ex26_target
|
||||
|
||||
rect = Rectangle(rad, rev)
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ development = [
|
|||
"black",
|
||||
"mypy",
|
||||
"pylint",
|
||||
"pytest",
|
||||
"pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
|
||||
"pytest-benchmark",
|
||||
"pytest-cov",
|
||||
"pytest-xdist",
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
|
|||
elif mode == Mode.INTERSECT:
|
||||
if self._obj is None:
|
||||
raise RuntimeError("Nothing to intersect with")
|
||||
combined = self._obj.intersect(*typed[self._shape])
|
||||
combined = self._obj.intersect(Compound(typed[self._shape]))
|
||||
elif mode == Mode.REPLACE:
|
||||
combined = self._sub_class(list(typed[self._shape]))
|
||||
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
|
|||
if self_intersection is None:
|
||||
self_intersection_area = 0.0
|
||||
else:
|
||||
self_intersection_area = self_intersection.area
|
||||
self_intersection_area = sum(f.area for f in self_intersection.faces())
|
||||
d_line += placed_label
|
||||
bbox_size = d_line.bounding_box().diagonal
|
||||
|
||||
|
|
@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
|
|||
if line_intersection is None:
|
||||
common_area = 0.0
|
||||
else:
|
||||
common_area = line_intersection.area
|
||||
common_area = sum(f.area for f in line_intersection.faces())
|
||||
common_area += self_intersection_area
|
||||
score = (d_line.area - 10 * common_area) / bbox_size
|
||||
d_lines[d_line] = score
|
||||
|
|
|
|||
|
|
@ -758,7 +758,7 @@ class ExportDXF(Export2D):
|
|||
)
|
||||
|
||||
# need to apply the transform on the geometry level
|
||||
if edge.wrapped is None or edge.location is None:
|
||||
if not edge or edge.location is None:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
t = edge.location.wrapped.Transformation()
|
||||
spline.Transform(t)
|
||||
|
|
@ -1345,7 +1345,7 @@ class ExportSVG(Export2D):
|
|||
u2 = adaptor.LastParameter()
|
||||
|
||||
# Apply the shape location to the geometry.
|
||||
if edge.wrapped is None or edge.location is None:
|
||||
if not edge or edge.location is None:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
t = edge.location.wrapped.Transformation()
|
||||
spline.Transform(t)
|
||||
|
|
@ -1411,7 +1411,7 @@ class ExportSVG(Export2D):
|
|||
}
|
||||
|
||||
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
||||
if edge.wrapped is None:
|
||||
if not edge:
|
||||
raise ValueError(f"Edge is empty {edge}.")
|
||||
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
|
||||
geom_type = edge.geom_type
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ class Mesher:
|
|||
ocp_mesh_vertices.append(pnt)
|
||||
|
||||
# Store the triangles from the triangulated faces
|
||||
if facet.wrapped is None:
|
||||
if not facet:
|
||||
continue
|
||||
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
|
||||
order = [1, 3, 2] if facet_reversed else [1, 2, 3]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ license:
|
|||
from __future__ import annotations
|
||||
|
||||
import copy as copy_module
|
||||
import warnings
|
||||
import numpy as np
|
||||
import sympy # type: ignore
|
||||
from collections.abc import Iterable
|
||||
|
|
@ -792,7 +793,7 @@ class FilletPolyline(BaseLineObject):
|
|||
|
||||
Args:
|
||||
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
|
||||
radius (float): fillet radius
|
||||
radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices
|
||||
close (bool, optional): close end points with extra Edge and corner fillets.
|
||||
Defaults to False
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
|
|
@ -807,7 +808,7 @@ class FilletPolyline(BaseLineObject):
|
|||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike | Iterable[VectorLike],
|
||||
radius: float,
|
||||
radius: float | Iterable[float],
|
||||
close: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
|
|
@ -818,8 +819,18 @@ class FilletPolyline(BaseLineObject):
|
|||
|
||||
if len(points) < 2:
|
||||
raise ValueError("FilletPolyline requires two or more pts")
|
||||
if radius <= 0:
|
||||
raise ValueError("radius must be positive")
|
||||
|
||||
if isinstance(radius, (int, float)):
|
||||
radius_list = [radius] * len(points) # Single radius for all points
|
||||
else:
|
||||
radius_list = list(radius)
|
||||
if len(radius_list) != len(points) - int(not close) * 2:
|
||||
raise ValueError(
|
||||
f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
|
||||
)
|
||||
for r in radius_list:
|
||||
if r <= 0:
|
||||
raise ValueError(f"radius {r} must be positive")
|
||||
|
||||
lines_pts = WorkplaneList.localize(*points)
|
||||
|
||||
|
|
@ -851,12 +862,14 @@ class FilletPolyline(BaseLineObject):
|
|||
|
||||
# For each corner vertex create a new fillet Edge
|
||||
fillets = []
|
||||
for vertex, edges in vertex_to_edges.items():
|
||||
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
|
||||
if len(edges) != 2:
|
||||
continue
|
||||
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
|
||||
third_edge = Edge.make_line(*[v for v in other_vertices])
|
||||
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex])
|
||||
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
|
||||
radius_list[i - int(not close)], [vertex]
|
||||
)
|
||||
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
||||
|
||||
# Create the Edges that join the fillets
|
||||
|
|
@ -1362,6 +1375,12 @@ class PointArcTangentLine(BaseEdgeObject):
|
|||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"The 'PointArcTangentLine' object is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
|
|
@ -1441,6 +1460,12 @@ class PointArcTangentArc(BaseEdgeObject):
|
|||
RuntimeError: No tangent arc found
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"The 'PointArcTangentArc' object is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
|
|
@ -1585,6 +1610,12 @@ class ArcArcTangentLine(BaseEdgeObject):
|
|||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
|
|
@ -1685,6 +1716,12 @@ class ArcArcTangentArc(BaseEdgeObject):
|
|||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"The 'ArcArcTangentArc' object is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ def chamfer(
|
|||
|
||||
if target._dim == 1:
|
||||
if isinstance(target, BaseLineObject):
|
||||
if target.wrapped is None:
|
||||
if not target:
|
||||
target = Wire([]) # empty wire
|
||||
else:
|
||||
target = Wire(target.wrapped)
|
||||
|
|
@ -465,7 +465,7 @@ def fillet(
|
|||
|
||||
if target._dim == 1:
|
||||
if isinstance(target, BaseLineObject):
|
||||
if target.wrapped is None:
|
||||
if not target:
|
||||
target = Wire([]) # empty wire
|
||||
else:
|
||||
target = Wire(target.wrapped)
|
||||
|
|
|
|||
|
|
@ -58,13 +58,12 @@ import copy
|
|||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from itertools import combinations
|
||||
from typing import Type, Union
|
||||
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from itertools import combinations
|
||||
from typing_extensions import Self
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
|
||||
from OCP.Font import (
|
||||
Font_FA_Bold,
|
||||
Font_FA_BoldItalic,
|
||||
|
|
@ -107,7 +106,6 @@ from build123d.geometry import (
|
|||
VectorLike,
|
||||
logger,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from .one_d import Edge, Wire, Mixin1D
|
||||
from .shape_core import (
|
||||
|
|
@ -130,7 +128,7 @@ from .utils import (
|
|||
from .zero_d import Vertex
|
||||
|
||||
|
||||
class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||
class Compound(Mixin3D[TopoDS_Compound]):
|
||||
"""A Compound in build123d is a topological entity representing a collection of
|
||||
geometric shapes grouped together within a single structure. It serves as a
|
||||
container for organizing diverse shapes like edges, faces, or solids. This
|
||||
|
|
@ -455,7 +453,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
will be a Wire, otherwise a Shape.
|
||||
"""
|
||||
if self._dim == 1:
|
||||
curve = Curve() if self.wrapped is None else Curve(self.wrapped)
|
||||
curve = Curve() if self._wrapped is None else Curve(self.wrapped)
|
||||
sum1d: Edge | Wire | ShapeList[Edge] = curve + other
|
||||
if isinstance(sum1d, ShapeList):
|
||||
result1d: Curve | Wire = Curve(sum1d)
|
||||
|
|
@ -517,7 +515,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
Check if empty.
|
||||
"""
|
||||
|
||||
return TopoDS_Iterator(self.wrapped).More()
|
||||
return self._wrapped is not None and TopoDS_Iterator(self.wrapped).More()
|
||||
|
||||
def __iter__(self) -> Iterator[Shape]:
|
||||
"""
|
||||
|
|
@ -534,7 +532,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
def __len__(self) -> int:
|
||||
"""Return the number of subshapes"""
|
||||
count = 0
|
||||
if self.wrapped is not None:
|
||||
if self._wrapped is not None:
|
||||
for _ in self:
|
||||
count += 1
|
||||
return count
|
||||
|
|
@ -602,7 +600,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
|
||||
def compounds(self) -> ShapeList[Compound]:
|
||||
"""compounds - all the compounds in this Shape"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return ShapeList()
|
||||
if isinstance(self.wrapped, TopoDS_Compound):
|
||||
# pylint: disable=not-an-iterable
|
||||
|
|
@ -651,11 +649,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
children[child_index_pair[1]]
|
||||
)
|
||||
if obj_intersection is not None:
|
||||
common_volume = (
|
||||
0.0
|
||||
if isinstance(obj_intersection, list)
|
||||
else obj_intersection.volume
|
||||
)
|
||||
common_volume = sum(s.volume for s in obj_intersection.solids())
|
||||
if common_volume > tolerance:
|
||||
return (
|
||||
True,
|
||||
|
|
@ -711,6 +705,148 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
|||
|
||||
return results
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
|
||||
"""Intersect Compound with Shape or geometry object
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
|
||||
faces, and/or solids.
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
|
||||
def bool_op(
|
||||
args: Sequence,
|
||||
tools: Sequence,
|
||||
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
|
||||
def expand_compound(compound: Compound) -> ShapeList:
|
||||
shapes = ShapeList(compound.children)
|
||||
for shape_type in [Vertex, Edge, Wire, Face, Shell, Solid]:
|
||||
shapes.extend(compound.get_type(shape_type))
|
||||
return shapes
|
||||
|
||||
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
|
||||
# Remove lower order shapes from list which *appear* to be part of
|
||||
# a higher order shape using a lazy distance check
|
||||
# (sufficient for vertices, may be an issue for higher orders)
|
||||
order_groups = []
|
||||
for order in orders:
|
||||
order_groups.append(
|
||||
ShapeList([s for s in shapes if isinstance(s, order)])
|
||||
)
|
||||
|
||||
filtered_shapes = order_groups[-1]
|
||||
for i in range(len(order_groups) - 1):
|
||||
los = order_groups[i]
|
||||
his: list = sum(order_groups[i + 1 :], [])
|
||||
filtered_shapes.extend(
|
||||
ShapeList(
|
||||
lo
|
||||
for lo in los
|
||||
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Shape] = expand_compound(self)
|
||||
target: ShapeList | Shape
|
||||
for other in to_intersect:
|
||||
# Conform target type
|
||||
match other:
|
||||
case Axis():
|
||||
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
|
||||
bbox = self.bounding_box()
|
||||
dist = self.distance_to(other.position)
|
||||
dist = dist if dist >= 1 else 1
|
||||
target = Edge.make_line(
|
||||
other.position - other.direction * bbox.diagonal * dist,
|
||||
other.position + other.direction * bbox.diagonal * dist,
|
||||
)
|
||||
case Plane():
|
||||
target = Face(other)
|
||||
case Vector():
|
||||
target = Vertex(other)
|
||||
case Location():
|
||||
target = Vertex(other.position)
|
||||
case Compound():
|
||||
target = expand_compound(other)
|
||||
case _ if issubclass(type(other), Shape):
|
||||
target = other
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||
|
||||
# Find common matches
|
||||
common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
|
||||
result: ShapeList
|
||||
for obj in common_set:
|
||||
if isinstance(target, Shape):
|
||||
target = ShapeList([target])
|
||||
result = ShapeList()
|
||||
for t in target:
|
||||
operation = BRepAlgoAPI_Section()
|
||||
result.extend(bool_op((obj,), (t,), operation))
|
||||
if (
|
||||
not isinstance(obj, Edge | Wire)
|
||||
and not isinstance(t, Edge | Wire)
|
||||
) or (
|
||||
isinstance(obj, Solid | Compound)
|
||||
or isinstance(t, Solid | Compound)
|
||||
):
|
||||
# Face + Edge combinations may produce an intersection
|
||||
# with Common but always with Section.
|
||||
# No easy way to deduplicate
|
||||
# Many Solid + Edge combinations need Common
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (t,), operation))
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
expanded: ShapeList = ShapeList()
|
||||
if common:
|
||||
for shape in common:
|
||||
if isinstance(shape, Compound):
|
||||
expanded.extend(expand_compound(shape))
|
||||
else:
|
||||
expanded.append(shape)
|
||||
|
||||
if expanded:
|
||||
common_set = ShapeList()
|
||||
for shape in expanded:
|
||||
if isinstance(shape, Wire):
|
||||
common_set.extend(shape.edges())
|
||||
elif isinstance(shape, Shell):
|
||||
common_set.extend(shape.faces())
|
||||
else:
|
||||
common_set.append(shape)
|
||||
common_set = to_vertex(set(to_vector(common_set)))
|
||||
common_set = filter_shapes_by_order(
|
||||
common_set, [Vertex, Edge, Face, Solid]
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
return ShapeList(common_set)
|
||||
|
||||
def unwrap(self, fully: bool = True) -> Self | Shape:
|
||||
"""Strip unnecessary Compound wrappers
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
|
|||
- Edge -> (QualifiedCurve, h2d, first, last, True)
|
||||
- Vector -> (CartesianPoint, None, None, None, False)
|
||||
"""
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
raise TypeError("Can't create a qualified curve from empty edge")
|
||||
|
||||
if isinstance(obj.wrapped, TopoDS_Edge):
|
||||
|
|
|
|||
|
|
@ -52,12 +52,11 @@ license:
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import numpy as np
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Sequence
|
||||
from itertools import combinations
|
||||
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias, overload
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
from typing import cast as tcast
|
||||
|
||||
import numpy as np
|
||||
|
|
@ -217,6 +216,7 @@ from build123d.geometry import (
|
|||
)
|
||||
|
||||
from .shape_core import (
|
||||
TOPODS,
|
||||
Shape,
|
||||
ShapeList,
|
||||
SkipClean,
|
||||
|
|
@ -250,7 +250,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
from .two_d import Face, Shell # pylint: disable=R0801
|
||||
|
||||
|
||||
class Mixin1D(Shape):
|
||||
class Mixin1D(Shape[TOPODS]):
|
||||
"""Methods to add to the Edge and Wire classes"""
|
||||
|
||||
# ---- Properties ----
|
||||
|
|
@ -263,14 +263,14 @@ class Mixin1D(Shape):
|
|||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Are the start and end points equal?"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't determine if empty Edge or Wire is closed")
|
||||
return BRep_Tool.IsClosed_s(self.wrapped)
|
||||
|
||||
@property
|
||||
def is_forward(self) -> bool:
|
||||
"""Does the Edge/Wire loop forward or reverse"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't determine direction of empty Edge or Wire")
|
||||
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
|
||||
|
||||
|
|
@ -388,8 +388,7 @@ class Mixin1D(Shape):
|
|||
shape
|
||||
# for o in (other if isinstance(other, (list, tuple)) else [other])
|
||||
for o in ([other] if isinstance(other, Shape) else other)
|
||||
if o is not None
|
||||
for shape in get_top_level_topods_shapes(o.wrapped)
|
||||
for shape in get_top_level_topods_shapes(o.wrapped if o else None)
|
||||
]
|
||||
# If there is nothing to add return the original object
|
||||
if not topods_summands:
|
||||
|
|
@ -404,7 +403,7 @@ class Mixin1D(Shape):
|
|||
)
|
||||
summand_edges = [e for summand in summands for e in summand.edges()]
|
||||
|
||||
if self.wrapped is None: # an empty object
|
||||
if self._wrapped is None: # an empty object
|
||||
if len(summands) == 1:
|
||||
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
|
||||
else:
|
||||
|
|
@ -452,7 +451,7 @@ class Mixin1D(Shape):
|
|||
Returns:
|
||||
Vector: center
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find center of empty edge/wire")
|
||||
|
||||
if center_of == CenterOf.GEOMETRY:
|
||||
|
|
@ -578,7 +577,7 @@ class Mixin1D(Shape):
|
|||
>>> show(my_wire, Curve(comb))
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't create curvature_comb for empty curve")
|
||||
pln = self.common_plane()
|
||||
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
|
||||
|
|
@ -729,122 +728,103 @@ class Mixin1D(Shape):
|
|||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
|
||||
common_set: ShapeList[Vertex | Edge] = ShapeList(self.edges())
|
||||
target: ShapeList | Shape | Plane
|
||||
def bool_op(
|
||||
args: Sequence,
|
||||
tools: Sequence,
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
|
||||
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
|
||||
# Remove lower order shapes from list which *appear* to be part of
|
||||
# a higher order shape using a lazy distance check
|
||||
# (sufficient for vertices, may be an issue for higher orders)
|
||||
order_groups = []
|
||||
for order in orders:
|
||||
order_groups.append(
|
||||
ShapeList([s for s in shapes if isinstance(s, order)])
|
||||
)
|
||||
|
||||
filtered_shapes = order_groups[-1]
|
||||
for i in range(len(order_groups) - 1):
|
||||
los = order_groups[i]
|
||||
his: list = sum(order_groups[i + 1 :], [])
|
||||
filtered_shapes.extend(
|
||||
ShapeList(
|
||||
lo
|
||||
for lo in los
|
||||
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Wire] = ShapeList([self])
|
||||
target: Shape | Plane
|
||||
for other in to_intersect:
|
||||
# Conform target type
|
||||
# Vertices need to be Vector for set()
|
||||
match other:
|
||||
case Axis():
|
||||
target = ShapeList([Edge(other)])
|
||||
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
|
||||
bbox = self.bounding_box()
|
||||
dist = self.distance_to(other.position)
|
||||
dist = dist if dist >= 1 else 1
|
||||
target = Edge.make_line(
|
||||
other.position - other.direction * bbox.diagonal * dist,
|
||||
other.position + other.direction * bbox.diagonal * dist,
|
||||
)
|
||||
case Plane():
|
||||
target = other
|
||||
case Vector():
|
||||
target = Vertex(other)
|
||||
case Location():
|
||||
target = Vertex(other.position)
|
||||
case Edge():
|
||||
target = ShapeList([other])
|
||||
case Wire():
|
||||
target = ShapeList(other.edges())
|
||||
case _ if issubclass(type(other), Shape):
|
||||
target = other
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||
|
||||
# Find common matches
|
||||
common: list[Vector | Edge] = []
|
||||
result: ShapeList | Shape | None
|
||||
common: list[Vertex | Edge | Wire] = []
|
||||
result: ShapeList | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case obj, Shape() as target:
|
||||
# Find Shape with Edge/Wire
|
||||
if isinstance(target, Vertex):
|
||||
result = Shape.intersect(obj, target)
|
||||
else:
|
||||
result = target.intersect(obj)
|
||||
case (_, Plane()):
|
||||
target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
|
||||
operation = BRepAlgoAPI_Section()
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation))
|
||||
|
||||
if result:
|
||||
if not isinstance(result, list):
|
||||
result = ShapeList([result])
|
||||
common.extend(to_vector(result))
|
||||
case (_, Vertex() | Edge() | Wire()):
|
||||
operation = BRepAlgoAPI_Section()
|
||||
section = bool_op((obj,), (target,), operation)
|
||||
result = section
|
||||
if not section:
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation))
|
||||
|
||||
case Vertex() as obj, target:
|
||||
if not isinstance(target, ShapeList):
|
||||
target = ShapeList([target])
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
for tar in target:
|
||||
if isinstance(tar, Edge):
|
||||
result = Shape.intersect(obj, tar)
|
||||
else:
|
||||
result = obj.intersect(tar)
|
||||
|
||||
if result:
|
||||
if not isinstance(result, list):
|
||||
result = ShapeList([result])
|
||||
common.extend(to_vector(result))
|
||||
|
||||
case Edge() as obj, ShapeList() as targets:
|
||||
# Find any edge / edge intersection points
|
||||
for tar in targets:
|
||||
# Find crossing points
|
||||
try:
|
||||
intersection_points = obj.find_intersection_points(tar)
|
||||
common.extend(intersection_points)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Find common end points
|
||||
obj_end_points = set(Vector(v) for v in obj.vertices())
|
||||
tar_end_points = set(Vector(v) for v in tar.vertices())
|
||||
points = set.intersection(obj_end_points, tar_end_points)
|
||||
common.extend(points)
|
||||
|
||||
# Find Edge/Edge overlaps
|
||||
result = obj._bool_op(
|
||||
(obj,), targets, BRepAlgoAPI_Common()
|
||||
).edges()
|
||||
common.extend(result if isinstance(result, list) else [result])
|
||||
|
||||
case Edge() as obj, Plane() as plane:
|
||||
# Find any edge / plane intersection points & edges
|
||||
# Find point intersections
|
||||
if obj.wrapped is None:
|
||||
continue
|
||||
geom_line = BRep_Tool.Curve_s(
|
||||
obj.wrapped, obj.param_at(0), obj.param_at(1)
|
||||
)
|
||||
geom_plane = Geom_Plane(plane.local_coord_system)
|
||||
intersection_calculator = GeomAPI_IntCS(geom_line, geom_plane)
|
||||
plane_intersection_points: list[Vector] = []
|
||||
if intersection_calculator.IsDone():
|
||||
plane_intersection_points = [
|
||||
Vector(intersection_calculator.Point(i + 1))
|
||||
for i in range(intersection_calculator.NbPoints())
|
||||
]
|
||||
common.extend(plane_intersection_points)
|
||||
|
||||
# Find edge intersections
|
||||
if all(
|
||||
plane.contains(v)
|
||||
for v in obj.positions(i / 7 for i in range(8))
|
||||
): # is a 2D edge
|
||||
common.append(obj)
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = to_vertex(set(common))
|
||||
# Remove Vertex intersections coincident to Edge intersections
|
||||
vts = common_set.vertices()
|
||||
eds = common_set.edges()
|
||||
if vts and eds:
|
||||
filtered_vts = ShapeList(
|
||||
[
|
||||
v
|
||||
for v in vts
|
||||
if all(v.distance_to(e) > TOLERANCE for e in eds)
|
||||
]
|
||||
)
|
||||
common_set = filtered_vts + eds
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
if isinstance(shape, Wire):
|
||||
common_set.extend(shape.edges())
|
||||
else:
|
||||
common_set.append(shape)
|
||||
common_set = to_vertex(set(to_vector(common_set)))
|
||||
common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -991,7 +971,7 @@ class Mixin1D(Shape):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find normal of empty edge/wire")
|
||||
|
||||
curve = self.geom_adaptor()
|
||||
|
|
@ -1225,7 +1205,7 @@ class Mixin1D(Shape):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None or face.wrapped is None:
|
||||
if self._wrapped is None or not face:
|
||||
raise ValueError("Can't project an empty Edge or Wire onto empty Face")
|
||||
|
||||
bldr = BRepProj_Projection(
|
||||
|
|
@ -1297,7 +1277,7 @@ class Mixin1D(Shape):
|
|||
|
||||
return edges
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't project empty edge/wire")
|
||||
|
||||
# Setup the projector
|
||||
|
|
@ -1400,7 +1380,7 @@ class Mixin1D(Shape):
|
|||
- **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 tool.wrapped is None:
|
||||
if self._wrapped is None or not tool:
|
||||
raise ValueError("Can't split an empty edge/wire/tool")
|
||||
|
||||
shape_list = TopTools_ListOfShape()
|
||||
|
|
@ -1566,7 +1546,7 @@ class Mixin1D(Shape):
|
|||
return Shape.get_shape_list(self, "Wire")
|
||||
|
||||
|
||||
class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||
class Edge(Mixin1D[TopoDS_Edge]):
|
||||
"""An Edge in build123d is a fundamental element in the topological data structure
|
||||
representing a one-dimensional geometric entity within a 3D model. It encapsulates
|
||||
information about a curve, which could be a line, arc, or other parametrically
|
||||
|
|
@ -1647,7 +1627,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
Returns:
|
||||
Edge: extruded shape
|
||||
"""
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
raise ValueError("Can't extrude empty vertex")
|
||||
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
|
||||
|
||||
|
|
@ -2538,7 +2518,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
extension_factor: float = 0.1,
|
||||
):
|
||||
"""Helper method to slightly extend an edge that is bound to a surface"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't extend empty spline")
|
||||
if self.geom_type != GeomType.BSPLINE:
|
||||
raise TypeError("_extend_spline only works with splines")
|
||||
|
|
@ -2595,7 +2575,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
Returns:
|
||||
ShapeList[Vector]: list of intersection points
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find intersections of empty edge")
|
||||
|
||||
# Convert an Axis into an edge at least as large as self and Axis start point
|
||||
|
|
@ -2723,7 +2703,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
|
||||
def geom_adaptor(self) -> BRepAdaptor_Curve:
|
||||
"""Return the Geom Curve from this Edge"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find adaptor for empty edge")
|
||||
return BRepAdaptor_Curve(self.wrapped)
|
||||
|
||||
|
|
@ -2811,7 +2791,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
float: Normalized parameter in [0.0, 1.0] corresponding to the point's
|
||||
closest location on the edge.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find param on empty edge")
|
||||
|
||||
pnt = Vector(point)
|
||||
|
|
@ -2945,7 +2925,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
Returns:
|
||||
Edge: reversed
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("An empty edge can't be reversed")
|
||||
|
||||
assert isinstance(self.wrapped, TopoDS_Edge)
|
||||
|
|
@ -3025,7 +3005,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
# if start_u >= end_u:
|
||||
# raise ValueError(f"start ({start_u}) must be less than end ({end_u})")
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't trim empty edge")
|
||||
|
||||
self_copy = copy.deepcopy(self)
|
||||
|
|
@ -3060,7 +3040,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
Returns:
|
||||
Edge: trimmed edge
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't trim empty edge")
|
||||
|
||||
start_u = Mixin1D._to_param(self, start, "start")
|
||||
|
|
@ -3089,7 +3069,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
|||
return Edge(new_edge)
|
||||
|
||||
|
||||
class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||
class Wire(Mixin1D[TopoDS_Wire]):
|
||||
"""A Wire in build123d is a topological entity representing a connected sequence
|
||||
of edges forming a continuous curve or path in 3D space. Wires are essential
|
||||
components in modeling complex objects, defining boundaries for surfaces or
|
||||
|
|
@ -3623,7 +3603,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
Returns:
|
||||
Wire: chamfered wire
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't chamfer empty wire")
|
||||
|
||||
reference_edge = edge
|
||||
|
|
@ -3638,7 +3618,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
)
|
||||
|
||||
for v in vertices:
|
||||
if v.wrapped is None:
|
||||
if not v:
|
||||
continue
|
||||
edge_list = vertex_edge_map.FindFromKey(v.wrapped)
|
||||
|
||||
|
|
@ -3695,7 +3675,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
Returns:
|
||||
Wire: filleted wire
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't fillet an empty wire")
|
||||
|
||||
# Create a face to fillet
|
||||
|
|
@ -3723,7 +3703,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
Returns:
|
||||
Wire: fixed wire
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't fix an empty edge")
|
||||
|
||||
sf_w = ShapeFix_Wireframe(self.wrapped)
|
||||
|
|
@ -3735,7 +3715,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
|
||||
def geom_adaptor(self) -> BRepAdaptor_CompCurve:
|
||||
"""Return the Geom Comp Curve for this Wire"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't get geom adaptor of empty wire")
|
||||
|
||||
return BRepAdaptor_CompCurve(self.wrapped)
|
||||
|
|
@ -3779,7 +3759,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
float: Normalized parameter in [0.0, 1.0] representing the relative
|
||||
position of the projected point along the wire.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find point on empty wire")
|
||||
|
||||
point_on_curve = Vector(point)
|
||||
|
|
@ -3932,7 +3912,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
if self.wrapped is None or target_object.wrapped is None:
|
||||
if self._wrapped is None or not target_object:
|
||||
raise ValueError("Can't project empty Wires or to empty Shapes")
|
||||
|
||||
if direction is not None and center is None:
|
||||
|
|
@ -4021,7 +4001,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
Returns:
|
||||
Wire: stitched wires
|
||||
"""
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
if self._wrapped is None or not other:
|
||||
raise ValueError("Can't stitch empty wires")
|
||||
|
||||
wire_builder = BRepBuilderAPI_MakeWire()
|
||||
|
|
@ -4065,7 +4045,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
|||
"""
|
||||
# Build a single Geom_BSplineCurve from the wire, in *topological order*
|
||||
builder = GeomConvert_CompCurveToBSplineCurve()
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't convert an empty wire")
|
||||
wire_explorer = BRepTools_WireExplorer(self.wrapped)
|
||||
|
||||
|
|
@ -4217,9 +4197,9 @@ def topo_explore_connected_edges(
|
|||
parent = parent if parent is not None else edge.topo_parent
|
||||
if parent is None:
|
||||
raise ValueError("edge has no valid parent")
|
||||
given_topods_edge = edge.wrapped
|
||||
if given_topods_edge is None:
|
||||
if not edge:
|
||||
raise ValueError("edge is empty")
|
||||
given_topods_edge = edge.wrapped
|
||||
connected_edges = set()
|
||||
|
||||
# Find all the TopoDS_Edges for this Shape
|
||||
|
|
@ -4262,11 +4242,11 @@ def topo_explore_connected_faces(
|
|||
) -> list[TopoDS_Face]:
|
||||
"""Given an edge extracted from a Shape, return the topods_faces connected to it"""
|
||||
|
||||
if edge.wrapped is None:
|
||||
if not edge:
|
||||
raise ValueError("Can't explore from an empty edge")
|
||||
|
||||
parent = parent if parent is not None else edge.topo_parent
|
||||
if parent is None or parent.wrapped is None:
|
||||
if not parent:
|
||||
raise ValueError("edge has no valid parent")
|
||||
|
||||
# make a edge --> faces mapping
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
color: ColorLike | None = None,
|
||||
parent: Compound | None = None,
|
||||
):
|
||||
self.wrapped: TOPODS | None = (
|
||||
self._wrapped: TOPODS | None = (
|
||||
tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None
|
||||
)
|
||||
self.for_construction = False
|
||||
|
|
@ -304,6 +304,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
|
||||
@property
|
||||
def wrapped(self):
|
||||
assert self._wrapped
|
||||
return self._wrapped
|
||||
|
||||
@wrapped.setter
|
||||
def wrapped(self, shape: TOPODS):
|
||||
self._wrapped = shape
|
||||
|
||||
def __bool__(self):
|
||||
return self._wrapped is not None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _dim(self) -> int | None:
|
||||
|
|
@ -312,7 +324,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def area(self) -> float:
|
||||
"""area -the surface area of all faces in this Shape"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return 0.0
|
||||
properties = GProp_GProps()
|
||||
BRepGProp.SurfaceProperties_s(self.wrapped, properties)
|
||||
|
|
@ -351,7 +363,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
GeomType: The geometry type of the shape
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot determine geometry type of an empty shape")
|
||||
|
||||
shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
|
||||
|
|
@ -380,7 +392,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
bool: is the shape manifold or water tight
|
||||
"""
|
||||
# Extract one or more (if a Compound) shape from self
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return False
|
||||
shape_stack = get_top_level_topods_shapes(self.wrapped)
|
||||
|
||||
|
|
@ -431,12 +443,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
underlying shape with the potential to be given a location and an
|
||||
orientation.
|
||||
"""
|
||||
return self.wrapped is None or self.wrapped.IsNull()
|
||||
return self._wrapped is None or self.wrapped.IsNull()
|
||||
|
||||
@property
|
||||
def is_planar_face(self) -> bool:
|
||||
"""Is the shape a planar face even though its geom_type may not be PLANE"""
|
||||
if self.wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
|
||||
if self._wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
|
||||
return False
|
||||
surface = BRep_Tool.Surface_s(self.wrapped)
|
||||
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
|
||||
|
|
@ -448,7 +460,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
|
||||
description of what is checked.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return True
|
||||
chk = BRepCheck_Analyzer(self.wrapped)
|
||||
chk.SetParallel(True)
|
||||
|
|
@ -474,7 +486,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def location(self) -> Location:
|
||||
"""Get this Shape's Location"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't find the location of an empty shape")
|
||||
return Location(self.wrapped.Location())
|
||||
|
||||
|
|
@ -518,7 +530,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
- It is commonly used in structural analysis, mechanical simulations,
|
||||
and physics-based motion calculations.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't calculate matrix for empty shape")
|
||||
properties = GProp_GProps()
|
||||
BRepGProp.VolumeProperties_s(self.wrapped, properties)
|
||||
|
|
@ -546,7 +558,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
@property
|
||||
def position(self) -> Vector:
|
||||
"""Get the position component of this Shape's Location"""
|
||||
if self.wrapped is None or self.location is None:
|
||||
if self._wrapped is None or self.location is None:
|
||||
raise ValueError("Can't find the position of an empty shape")
|
||||
return self.location.position
|
||||
|
||||
|
|
@ -575,7 +587,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(Vector(0, 1, 0), 1000.0),
|
||||
(Vector(0, 0, 1), 300.0)]
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't calculate properties for empty shape")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -615,7 +627,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(150.0, 200.0, 50.0)
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't calculate moments for empty shape")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -785,7 +797,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
return 0.0
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -805,7 +817,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
],
|
||||
) -> ShapeList:
|
||||
"""Helper to extract entities of a specific type from a shape."""
|
||||
if shape.wrapped is None:
|
||||
if not shape:
|
||||
return ShapeList()
|
||||
shape_list = ShapeList(
|
||||
[shape.__class__.cast(i) for i in shape.entities(entity_type)]
|
||||
|
|
@ -859,7 +871,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
if not all(summand._dim == addend_dim for summand in summands):
|
||||
raise ValueError("Only shapes with the same dimension can be added")
|
||||
|
||||
if self.wrapped is None: # an empty object
|
||||
if self._wrapped is None: # an empty object
|
||||
if len(summands) == 1:
|
||||
sum_shape = summands[0]
|
||||
else:
|
||||
|
|
@ -876,7 +888,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"""intersect shape with self operator &"""
|
||||
others = other if isinstance(other, (list, tuple)) else [other]
|
||||
|
||||
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
|
||||
if not self or (isinstance(other, Shape) and not other):
|
||||
raise ValueError("Cannot intersect shape with empty compound")
|
||||
new_shape = self.intersect(*others)
|
||||
|
||||
|
|
@ -948,7 +960,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def __hash__(self) -> int:
|
||||
"""Return hash code"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return 0
|
||||
return hash(self.wrapped)
|
||||
|
||||
|
|
@ -966,7 +978,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
|
||||
"""cut shape from self operator -"""
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot subtract shape from empty compound")
|
||||
|
||||
# Convert `other` to list of base objects and filter out None values
|
||||
|
|
@ -1014,7 +1026,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
BoundBox: A box sized to contain this Shape
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return BoundBox(Bnd_Box())
|
||||
tolerance = TOLERANCE if tolerance is None else tolerance
|
||||
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
|
||||
|
|
@ -1033,7 +1045,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: Original object with extraneous internal edges removed
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
|
||||
upgrader.AllowInternalEdges(False)
|
||||
|
|
@ -1112,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
if self._wrapped is None or not other:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
|
||||
|
|
@ -1125,7 +1137,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
self, other: Shape | VectorLike
|
||||
) -> tuple[float, Vector, Vector]:
|
||||
"""Minimal distance between two shapes and the points on each shape"""
|
||||
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
|
||||
if self._wrapped is None or (isinstance(other, Shape) and not other):
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
if isinstance(other, Shape):
|
||||
|
|
@ -1155,14 +1167,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
|
||||
dist_calc = BRepExtrema_DistShapeShape()
|
||||
dist_calc.LoadS1(self.wrapped)
|
||||
|
||||
for other_shape in others:
|
||||
if other_shape.wrapped is None:
|
||||
if not other_shape:
|
||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||
dist_calc.LoadS2(other_shape.wrapped)
|
||||
dist_calc.Perform()
|
||||
|
|
@ -1181,7 +1193,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||
"""Return all of the TopoDS sub entities of the given type"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return []
|
||||
return _topods_entities(self.wrapped, topo_type)
|
||||
|
||||
|
|
@ -1209,7 +1221,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
list[Face]: A list of intersected faces sorted by distance from axis.position
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return ShapeList()
|
||||
|
||||
line = gce_MakeLin(axis.wrapped).Value()
|
||||
|
|
@ -1239,7 +1251,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def fix(self) -> Self:
|
||||
"""fix - try to fix shape if not valid"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
if not self.is_valid:
|
||||
shape_copy: Shape = copy.deepcopy(self, None)
|
||||
|
|
@ -1281,7 +1293,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
# self, child_type: Shapes, parent_type: Shapes
|
||||
# ) -> Dict[Shape, list[Shape]]:
|
||||
# """This function is very slow on M1 macs and is currently unused"""
|
||||
# if self.wrapped is None:
|
||||
# if self._wrapped is None:
|
||||
# return {}
|
||||
|
||||
# res = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
|
|
@ -1319,7 +1331,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
(e.g., edges, vertices) and other compounds, the method returns a list
|
||||
of only the simple shapes directly contained at the top level.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return ShapeList()
|
||||
return ShapeList(
|
||||
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
|
||||
|
|
@ -1327,7 +1339,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | Self | ShapeList[Self]:
|
||||
) -> None | ShapeList[Self]:
|
||||
"""Intersection of the arguments and this shape
|
||||
|
||||
Args:
|
||||
|
|
@ -1335,8 +1347,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
intersect with
|
||||
|
||||
Returns:
|
||||
Self | ShapeList[Self]: Resulting object may be of a different class than self
|
||||
or a ShapeList if multiple non-Compound object created
|
||||
None | ShapeList[Self]: Resulting ShapeList may contain different class
|
||||
than self
|
||||
"""
|
||||
|
||||
def _to_vertex(vec: Vector) -> Vertex:
|
||||
|
|
@ -1380,15 +1392,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
|
||||
# Find the shape intersections
|
||||
intersect_op = BRepAlgoAPI_Common()
|
||||
shape_intersections = self._bool_op((self,), objs, intersect_op)
|
||||
if isinstance(shape_intersections, ShapeList) and not shape_intersections:
|
||||
return None
|
||||
if (
|
||||
not isinstance(shape_intersections, ShapeList)
|
||||
and shape_intersections.is_null
|
||||
):
|
||||
return None
|
||||
return shape_intersections
|
||||
intersections = self._bool_op((self,), objs, intersect_op)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or None
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return None
|
||||
|
||||
def is_equal(self, other: Shape) -> bool:
|
||||
"""Returns True if two shapes are equal, i.e. if they share the same
|
||||
|
|
@ -1401,7 +1410,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
if self._wrapped is None or not other:
|
||||
return False
|
||||
return self.wrapped.IsEqual(other.wrapped)
|
||||
|
||||
|
|
@ -1416,7 +1425,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
if self._wrapped is None or not other:
|
||||
return False
|
||||
return self.wrapped.IsSame(other.wrapped)
|
||||
|
||||
|
|
@ -1429,7 +1438,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot locate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot locate a shape at an empty location")
|
||||
|
|
@ -1448,7 +1457,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of Shape at location
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot locate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot locate a shape at an empty location")
|
||||
|
|
@ -1466,7 +1475,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot mesh an empty shape")
|
||||
|
||||
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
|
||||
|
|
@ -1487,7 +1496,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
if not mirror_plane:
|
||||
mirror_plane = Plane.XY
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
transformation = gp_Trsf()
|
||||
transformation.SetMirror(
|
||||
|
|
@ -1505,7 +1514,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot move an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot move a shape at an empty location")
|
||||
|
|
@ -1525,7 +1534,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of Shape moved to relative location
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot move an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot move a shape at an empty location")
|
||||
|
|
@ -1539,7 +1548,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
OrientedBoundBox: A box oriented and sized to contain this Shape
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return OrientedBoundBox(Bnd_OBB())
|
||||
return OrientedBoundBox(self)
|
||||
|
||||
|
|
@ -1641,7 +1650,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
- The radius of gyration is computed based on the shape’s mass properties.
|
||||
- It is useful for evaluating structural stability and rotational behavior.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't calculate radius of gyration for empty shape")
|
||||
|
||||
properties = GProp_GProps()
|
||||
|
|
@ -1660,7 +1669,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot relocate an empty shape")
|
||||
if loc.wrapped is None:
|
||||
raise ValueError("Cannot relocate a shape at an empty location")
|
||||
|
|
@ -1855,7 +1864,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
"keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH"
|
||||
)
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot split an empty shape")
|
||||
|
||||
# Process the perimeter
|
||||
|
|
@ -1863,7 +1872,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
raise ValueError("perimeter must be a closed Wire or Edge")
|
||||
perimeter_edges = TopTools_SequenceOfShape()
|
||||
for perimeter_edge in perimeter.edges():
|
||||
if perimeter_edge.wrapped is None:
|
||||
if not perimeter_edge:
|
||||
continue
|
||||
perimeter_edges.Append(perimeter_edge.wrapped)
|
||||
|
||||
|
|
@ -1871,7 +1880,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
lefts: list[Shell] = []
|
||||
rights: list[Shell] = []
|
||||
for target_shell in self.shells():
|
||||
if target_shell.wrapped is None:
|
||||
if not target_shell:
|
||||
continue
|
||||
constructor = BRepFeat_SplitShape(target_shell.wrapped)
|
||||
constructor.Add(perimeter_edges)
|
||||
|
|
@ -1900,7 +1909,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
self, tolerance: float, angular_tolerance: float = 0.1
|
||||
) -> tuple[list[Vector], list[tuple[int, int, int]]]:
|
||||
"""General triangulated approximation"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot tessellate an empty shape")
|
||||
|
||||
self.mesh(tolerance, angular_tolerance)
|
||||
|
|
@ -1962,7 +1971,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Self: Approximated shape
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot approximate an empty shape")
|
||||
|
||||
params = ShapeCustom_RestrictionParameters()
|
||||
|
|
@ -1999,7 +2008,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: a copy of the object, but with geometry transformed
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
new_shape = copy.deepcopy(self, None)
|
||||
transformed = downcast(
|
||||
|
|
@ -2022,7 +2031,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of transformed shape with all objects keeping their type
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
new_shape = copy.deepcopy(self, None)
|
||||
transformed = downcast(
|
||||
|
|
@ -2095,7 +2104,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
Shape: copy of transformed Shape
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return self
|
||||
shape_copy: Shape = copy.deepcopy(self, None)
|
||||
transformed_shape = BRepBuilderAPI_Transform(
|
||||
|
|
@ -2126,7 +2135,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
args = list(args)
|
||||
tools = list(tools)
|
||||
# Find the highest order class from all the inputs Solid > Vertex
|
||||
order_dict = {type(s): type(s).order for s in [self] + args + tools}
|
||||
order_dict = {
|
||||
type(s): type(s).order
|
||||
for s in [self] + args + tools
|
||||
if hasattr(type(s), "order")
|
||||
}
|
||||
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
|
||||
|
||||
# The base of the operation
|
||||
|
|
@ -2200,7 +2213,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
|||
Returns:
|
||||
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
|
||||
"""
|
||||
if self.wrapped is None or other.wrapped is None:
|
||||
if self._wrapped is None or not other:
|
||||
return (ShapeList(), ShapeList())
|
||||
|
||||
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
|
||||
|
|
@ -2701,15 +2714,16 @@ class ShapeList(list[T]):
|
|||
tol_digits,
|
||||
)
|
||||
|
||||
elif hasattr(group_by, "wrapped"):
|
||||
if group_by.wrapped is None:
|
||||
raise ValueError("Cannot group by an empty object")
|
||||
elif not group_by:
|
||||
raise ValueError("Cannot group by an empty object")
|
||||
|
||||
if isinstance(group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
|
||||
elif hasattr(group_by, "wrapped") and isinstance(
|
||||
group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
|
||||
):
|
||||
|
||||
def key_f(obj):
|
||||
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
||||
return round(group_by.param_at_point(pnt1), tol_digits)
|
||||
def key_f(obj):
|
||||
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
||||
return round(group_by.param_at_point(pnt1), tol_digits)
|
||||
|
||||
elif isinstance(group_by, SortBy):
|
||||
if group_by == SortBy.LENGTH:
|
||||
|
|
@ -2815,22 +2829,22 @@ class ShapeList(list[T]):
|
|||
).position.Z,
|
||||
reverse=reverse,
|
||||
)
|
||||
elif hasattr(sort_by, "wrapped"):
|
||||
if sort_by.wrapped is None:
|
||||
raise ValueError("Cannot sort by an empty object")
|
||||
elif not sort_by:
|
||||
raise ValueError("Cannot sort by an empty object")
|
||||
elif hasattr(sort_by, "wrapped") and isinstance(
|
||||
sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
|
||||
):
|
||||
|
||||
if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
|
||||
def u_of_closest_center(obj) -> float:
|
||||
"""u-value of closest point between object center and sort_by"""
|
||||
assert not isinstance(sort_by, SortBy)
|
||||
pnt1, _pnt2 = sort_by.closest_points(obj.center())
|
||||
return sort_by.param_at_point(pnt1)
|
||||
|
||||
def u_of_closest_center(obj) -> float:
|
||||
"""u-value of closest point between object center and sort_by"""
|
||||
assert not isinstance(sort_by, SortBy)
|
||||
pnt1, _pnt2 = sort_by.closest_points(obj.center())
|
||||
return sort_by.param_at_point(pnt1)
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
objects = sorted(
|
||||
self, key=lambda o: u_of_closest_center(o), reverse=reverse
|
||||
)
|
||||
# pylint: disable=unnecessary-lambda
|
||||
objects = sorted(
|
||||
self, key=lambda o: u_of_closest_center(o), reverse=reverse
|
||||
)
|
||||
|
||||
elif isinstance(sort_by, SortBy):
|
||||
if sort_by == SortBy.LENGTH:
|
||||
|
|
|
|||
|
|
@ -56,13 +56,13 @@ from __future__ import annotations
|
|||
|
||||
import platform
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
from math import radians, cos, tan
|
||||
from typing import Union, TYPE_CHECKING
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
||||
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
||||
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
||||
|
|
@ -95,6 +95,7 @@ from OCP.gp import gp_Ax2, gp_Pnt
|
|||
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
||||
from build123d.geometry import (
|
||||
DEG2RAD,
|
||||
TOLERANCE,
|
||||
Axis,
|
||||
BoundBox,
|
||||
Color,
|
||||
|
|
@ -104,10 +105,9 @@ from build123d.geometry import (
|
|||
Vector,
|
||||
VectorLike,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from .one_d import Edge, Wire, Mixin1D
|
||||
from .shape_core import Shape, ShapeList, Joint, downcast, shapetype
|
||||
from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
|
||||
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
||||
from .utils import (
|
||||
_extrude_topods_shape,
|
||||
|
|
@ -122,7 +122,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801
|
||||
|
||||
|
||||
class Mixin3D(Shape):
|
||||
class Mixin3D(Shape[TOPODS]):
|
||||
"""Additional methods to add to 3D Shape classes"""
|
||||
|
||||
project_to_viewport = Mixin1D.project_to_viewport
|
||||
|
|
@ -420,6 +420,130 @@ class Mixin3D(Shape):
|
|||
|
||||
return return_value
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge | Face | Solid]:
|
||||
"""Intersect Solid with Shape or geometry object
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
|
||||
faces, and/or solids.
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
|
||||
def bool_op(
|
||||
args: Sequence,
|
||||
tools: Sequence,
|
||||
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
|
||||
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
|
||||
# Remove lower order shapes from list which *appear* to be part of
|
||||
# a higher order shape using a lazy distance check
|
||||
# (sufficient for vertices, may be an issue for higher orders)
|
||||
order_groups = []
|
||||
for order in orders:
|
||||
order_groups.append(
|
||||
ShapeList([s for s in shapes if isinstance(s, order)])
|
||||
)
|
||||
|
||||
filtered_shapes = order_groups[-1]
|
||||
for i in range(len(order_groups) - 1):
|
||||
los = order_groups[i]
|
||||
his: list = sum(order_groups[i + 1 :], [])
|
||||
filtered_shapes.extend(
|
||||
ShapeList(
|
||||
lo
|
||||
for lo in los
|
||||
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList([self])
|
||||
target: Shape
|
||||
for other in to_intersect:
|
||||
# Conform target type
|
||||
match other:
|
||||
case Axis():
|
||||
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
|
||||
bbox = self.bounding_box()
|
||||
dist = self.distance_to(other.position)
|
||||
dist = dist if dist >= 1 else 1
|
||||
target = Edge.make_line(
|
||||
other.position - other.direction * bbox.diagonal * dist,
|
||||
other.position + other.direction * bbox.diagonal * dist,
|
||||
)
|
||||
case Plane():
|
||||
target = Face(other)
|
||||
case Vector():
|
||||
target = Vertex(other)
|
||||
case Location():
|
||||
target = Vertex(other.position)
|
||||
case _ if issubclass(type(other), Shape):
|
||||
target = other
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||
|
||||
# Find common matches
|
||||
common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
|
||||
result: ShapeList | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
|
||||
operation = BRepAlgoAPI_Section()
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
if (
|
||||
not isinstance(obj, Edge | Wire)
|
||||
and not isinstance(target, (Edge | Wire))
|
||||
) or (isinstance(obj, Solid) or isinstance(target, Solid)):
|
||||
# Face + Edge combinations may produce an intersection
|
||||
# with Common but always with Section.
|
||||
# No easy way to deduplicate
|
||||
# Many Solid + Edge combinations need Common
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation))
|
||||
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
if isinstance(shape, Wire):
|
||||
common_set.extend(shape.edges())
|
||||
elif isinstance(shape, Shell):
|
||||
common_set.extend(shape.faces())
|
||||
else:
|
||||
common_set.append(shape)
|
||||
common_set = to_vertex(set(to_vector(common_set)))
|
||||
common_set = filter_shapes_by_order(
|
||||
common_set, [Vertex, Edge, Face, Solid]
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
return ShapeList(common_set)
|
||||
|
||||
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
|
||||
"""Returns whether or not the point is inside a solid or compound
|
||||
object within the specified tolerance.
|
||||
|
|
@ -590,7 +714,7 @@ class Mixin3D(Shape):
|
|||
return Shape.get_shape_list(self, "Solid")
|
||||
|
||||
|
||||
class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||
class Solid(Mixin3D[TopoDS_Solid]):
|
||||
"""A Solid in build123d represents a three-dimensional solid geometry
|
||||
in a topological structure. A solid is a closed and bounded volume, enclosing
|
||||
a region in 3D space. It comprises faces, edges, and vertices connected in a
|
||||
|
|
@ -1269,7 +1393,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
|||
outer_wire = section
|
||||
inner_wires = inner_wires if inner_wires else []
|
||||
|
||||
shapes = []
|
||||
shapes: list[Mixin3D[TopoDS_Shape]] = []
|
||||
for wire in [outer_wire] + inner_wires:
|
||||
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ import OCP.TopAbs as ta
|
|||
from OCP.BRep import BRep_Builder, BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||
from OCP.BRepAlgo import BRepAlgo
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakeEdge,
|
||||
BRepBuilderAPI_MakeFace,
|
||||
|
|
@ -139,6 +139,7 @@ from build123d.geometry import (
|
|||
|
||||
from .one_d import Edge, Mixin1D, Wire
|
||||
from .shape_core import (
|
||||
TOPODS,
|
||||
Shape,
|
||||
ShapeList,
|
||||
SkipClean,
|
||||
|
|
@ -165,7 +166,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
T = TypeVar("T", Edge, Wire, "Face")
|
||||
|
||||
|
||||
class Mixin2D(ABC, Shape):
|
||||
class Mixin2D(ABC, Shape[TOPODS]):
|
||||
"""Additional methods to add to Face and Shell class"""
|
||||
|
||||
project_to_viewport = Mixin1D.project_to_viewport
|
||||
|
|
@ -213,7 +214,7 @@ class Mixin2D(ABC, Shape):
|
|||
|
||||
def __neg__(self) -> Self:
|
||||
"""Reverse normal operator -"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Invalid Shape")
|
||||
new_surface = copy.deepcopy(self)
|
||||
new_surface.wrapped = downcast(self.wrapped.Complemented())
|
||||
|
|
@ -244,7 +245,7 @@ class Mixin2D(ABC, Shape):
|
|||
Returns:
|
||||
list[tuple[Vector, Vector]]: Point and normal of intersection
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return []
|
||||
|
||||
intersection_line = gce_MakeLin(other.wrapped).Value()
|
||||
|
|
@ -278,6 +279,126 @@ class Mixin2D(ABC, Shape):
|
|||
|
||||
return result
|
||||
|
||||
def intersect(
|
||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||
) -> None | ShapeList[Vertex | Edge | Face]:
|
||||
"""Intersect Face with Shape or geometry object
|
||||
|
||||
Args:
|
||||
to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
|
||||
|
||||
Returns:
|
||||
ShapeList[Vertex | Edge | Face] | None: ShapeList of vertices, edges, and/or
|
||||
faces.
|
||||
"""
|
||||
|
||||
def to_vector(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
|
||||
|
||||
def to_vertex(objs: Iterable) -> ShapeList:
|
||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||
|
||||
def bool_op(
|
||||
args: Sequence,
|
||||
tools: Sequence,
|
||||
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
|
||||
) -> ShapeList:
|
||||
# Wrap Shape._bool_op for corrected output
|
||||
intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
|
||||
if isinstance(intersections, ShapeList):
|
||||
return intersections or ShapeList()
|
||||
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||
return ShapeList([intersections])
|
||||
return ShapeList()
|
||||
|
||||
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
|
||||
# Remove lower order shapes from list which *appear* to be part of
|
||||
# a higher order shape using a lazy distance check
|
||||
# (sufficient for vertices, may be an issue for higher orders)
|
||||
order_groups = []
|
||||
for order in orders:
|
||||
order_groups.append(
|
||||
ShapeList([s for s in shapes if isinstance(s, order)])
|
||||
)
|
||||
|
||||
filtered_shapes = order_groups[-1]
|
||||
for i in range(len(order_groups) - 1):
|
||||
los = order_groups[i]
|
||||
his: list = sum(order_groups[i + 1 :], [])
|
||||
filtered_shapes.extend(
|
||||
ShapeList(
|
||||
lo
|
||||
for lo in los
|
||||
if all(lo.distance_to(hi) > TOLERANCE for hi in his)
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_shapes
|
||||
|
||||
common_set: ShapeList[Vertex | Edge | Face | Shell] = ShapeList([self])
|
||||
target: Shape
|
||||
for other in to_intersect:
|
||||
# Conform target type
|
||||
match other:
|
||||
case Axis():
|
||||
# BRepAlgoAPI_Section seems happier if Edge isnt infinite
|
||||
bbox = self.bounding_box()
|
||||
dist = self.distance_to(other.position)
|
||||
dist = dist if dist >= 1 else 1
|
||||
target = Edge.make_line(
|
||||
other.position - other.direction * bbox.diagonal * dist,
|
||||
other.position + other.direction * bbox.diagonal * dist,
|
||||
)
|
||||
case Plane():
|
||||
target = Face(other)
|
||||
case Vector():
|
||||
target = Vertex(other)
|
||||
case Location():
|
||||
target = Vertex(other.position)
|
||||
case _ if issubclass(type(other), Shape):
|
||||
target = other
|
||||
case _:
|
||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||
|
||||
# Find common matches
|
||||
common: list[Vertex | Edge | Wire | Face | Shell] = []
|
||||
result: ShapeList | None
|
||||
for obj in common_set:
|
||||
match (obj, target):
|
||||
case (_, Vertex() | Edge() | Wire() | Face() | Shell()):
|
||||
operation = BRepAlgoAPI_Section()
|
||||
result = bool_op((obj,), (target,), operation)
|
||||
if not isinstance(obj, Edge | Wire) and not isinstance(
|
||||
target, (Edge | Wire)
|
||||
):
|
||||
# Face + Edge combinations may produce an intersection
|
||||
# with Common but always with Section.
|
||||
# No easy way to deduplicate
|
||||
operation = BRepAlgoAPI_Common()
|
||||
result.extend(bool_op((obj,), (target,), operation))
|
||||
|
||||
case _ if issubclass(type(target), Shape):
|
||||
result = target.intersect(obj)
|
||||
|
||||
if result:
|
||||
common.extend(result)
|
||||
|
||||
if common:
|
||||
common_set = ShapeList()
|
||||
for shape in common:
|
||||
if isinstance(shape, Wire):
|
||||
common_set.extend(shape.edges())
|
||||
elif isinstance(shape, Shell):
|
||||
common_set.extend(shape.faces())
|
||||
else:
|
||||
common_set.append(shape)
|
||||
common_set = to_vertex(set(to_vector(common_set)))
|
||||
common_set = filter_shapes_by_order(common_set, [Vertex, Edge, Face])
|
||||
else:
|
||||
return None
|
||||
|
||||
return ShapeList(common_set)
|
||||
|
||||
@abstractmethod
|
||||
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
||||
"""A location from a face or shell"""
|
||||
|
|
@ -350,7 +471,7 @@ class Mixin2D(ABC, Shape):
|
|||
world_point, world_point - target_object_center
|
||||
)
|
||||
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't wrap around an empty face")
|
||||
|
||||
# Initial setup
|
||||
|
|
@ -411,7 +532,7 @@ class Mixin2D(ABC, Shape):
|
|||
raise RuntimeError(
|
||||
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
|
||||
)
|
||||
if wrapped_edge.wrapped is None or not wrapped_edge.is_valid:
|
||||
if not wrapped_edge or not wrapped_edge.is_valid:
|
||||
raise RuntimeError("Wrapped edge is invalid")
|
||||
|
||||
if not snap_to_face:
|
||||
|
|
@ -434,7 +555,7 @@ class Mixin2D(ABC, Shape):
|
|||
return projected_edge
|
||||
|
||||
|
||||
class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||
class Face(Mixin2D[TopoDS_Face]):
|
||||
"""A Face in build123d represents a 3D bounded surface within the topological data
|
||||
structure. It encapsulates geometric information, defining a face of a 3D shape.
|
||||
These faces are integral components of complex structures, such as solids and
|
||||
|
|
@ -449,7 +570,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
obj: TopoDS_Face,
|
||||
obj: TopoDS_Face | Plane,
|
||||
label: str = "",
|
||||
color: Color | None = None,
|
||||
parent: Compound | None = None,
|
||||
|
|
@ -457,7 +578,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
|
||||
|
||||
Args:
|
||||
obj (TopoDS_Shape, optional): OCCT Face.
|
||||
obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
|
||||
label (str, optional): Defaults to ''.
|
||||
color (Color, optional): Defaults to None.
|
||||
parent (Compound, optional): assembly parent. Defaults to None.
|
||||
|
|
@ -487,7 +608,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
|
||||
if args:
|
||||
l_a = len(args)
|
||||
if isinstance(args[0], TopoDS_Shape):
|
||||
if isinstance(args[0], Plane):
|
||||
obj = args[0]
|
||||
elif isinstance(args[0], TopoDS_Shape):
|
||||
obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
|
||||
elif isinstance(args[0], Wire):
|
||||
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
|
||||
|
|
@ -516,6 +639,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
color = kwargs.get("color", color)
|
||||
parent = kwargs.get("parent", parent)
|
||||
|
||||
if isinstance(obj, Plane):
|
||||
obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
|
||||
|
||||
if outer_wire is not None:
|
||||
inner_topods_wires = (
|
||||
[w.wrapped for w in inner_wires] if inner_wires is not None else []
|
||||
|
|
@ -545,7 +671,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
float: The total surface area, including the area of holes. Returns 0.0 if
|
||||
the face is empty.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
return 0.0
|
||||
|
||||
return self.without_holes().area
|
||||
|
|
@ -605,7 +731,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
ValueError: If the face or its underlying representation is empty.
|
||||
ValueError: If the face is not planar.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't determine axes_of_symmetry of empty face")
|
||||
|
||||
if not self.is_planar_face:
|
||||
|
|
@ -671,15 +797,13 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
).sort_by(Axis(cog, cross_dir))
|
||||
|
||||
bottom_area = sum(f.area for f in bottom_list)
|
||||
intersect_area = 0.0
|
||||
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
|
||||
intersection = flipped_face.intersect(bottom_face)
|
||||
if intersection is None or isinstance(intersection, list):
|
||||
if intersection is None:
|
||||
intersect_area = -1.0
|
||||
break
|
||||
else:
|
||||
assert isinstance(intersection, Face)
|
||||
intersect_area += intersection.area
|
||||
intersect_area = sum(f.area for f in intersection.faces())
|
||||
|
||||
if intersect_area == -1.0:
|
||||
continue
|
||||
|
|
@ -871,7 +995,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
Returns:
|
||||
Face: extruded shape
|
||||
"""
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
raise ValueError("Can't extrude empty object")
|
||||
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
|
||||
|
||||
|
|
@ -981,7 +1105,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
)
|
||||
return single_point_curve
|
||||
|
||||
if shape.wrapped is None:
|
||||
if not shape:
|
||||
raise ValueError("input Edge cannot be empty")
|
||||
|
||||
adaptor = BRepAdaptor_Curve(shape.wrapped)
|
||||
|
|
@ -1015,6 +1139,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
plane: Plane = Plane.XY,
|
||||
) -> Face:
|
||||
"""Create a unlimited size Face aligned with plane"""
|
||||
warnings.warn(
|
||||
"The 'make_plane' method is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
|
||||
return cls(pln_shape)
|
||||
|
||||
|
|
@ -1104,7 +1234,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
raise ValueError("exterior must be a Wire or list of Edges")
|
||||
|
||||
for edge in outside_edges:
|
||||
if edge.wrapped is None:
|
||||
if not edge:
|
||||
raise ValueError("exterior contains empty edges")
|
||||
surface.Add(edge.wrapped, GeomAbs_C0)
|
||||
|
||||
|
|
@ -1135,7 +1265,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
if interior_wires:
|
||||
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
|
||||
for wire in interior_wires:
|
||||
if wire.wrapped is None:
|
||||
if not wire:
|
||||
raise ValueError("interior_wires contain an empty wire")
|
||||
makeface_object.Add(wire.wrapped)
|
||||
try:
|
||||
|
|
@ -1329,7 +1459,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
) from err
|
||||
|
||||
result = result.fix()
|
||||
if not result.is_valid or result.wrapped is None:
|
||||
if not result.is_valid or not result:
|
||||
raise RuntimeError("Non planar face is invalid")
|
||||
|
||||
return result
|
||||
|
|
@ -1940,7 +2070,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot approximate an empty shape")
|
||||
|
||||
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
|
||||
|
|
@ -1953,7 +2083,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
Returns:
|
||||
Face: A new Face instance identical to the original but without any holes.
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Cannot remove holes from an empty face")
|
||||
|
||||
if not (inner_wires := self.inner_wires()):
|
||||
|
|
@ -2327,7 +2457,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
|||
return wrapped_wire
|
||||
|
||||
|
||||
class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||
class Shell(Mixin2D[TopoDS_Shell]):
|
||||
"""A Shell is a fundamental component in build123d's topological data structure
|
||||
representing a connected set of faces forming a closed surface in 3D space. As
|
||||
part of a geometric model, it defines a watertight enclosure, commonly encountered
|
||||
|
|
@ -2359,7 +2489,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
|||
obj = obj_list[0]
|
||||
|
||||
if isinstance(obj, Face):
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
raise ValueError(f"Can't create a Shell from empty Face")
|
||||
builder = BRep_Builder()
|
||||
shell = TopoDS_Shell()
|
||||
|
|
|
|||
|
|
@ -263,7 +263,10 @@ def _make_topods_face_from_wires(
|
|||
for inner_wire in inner_wires:
|
||||
if not BRep_Tool.IsClosed_s(inner_wire):
|
||||
raise ValueError("Cannot build face(s): inner wire is not closed")
|
||||
face_builder.Add(inner_wire)
|
||||
sf_s = ShapeFix_Shape(inner_wire)
|
||||
sf_s.Perform()
|
||||
fixed_inner_wire = TopoDS.Wire_s(sf_s.Shape())
|
||||
face_builder.Add(fixed_inner_wire)
|
||||
|
||||
face_builder.Build()
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def to_vtk_poly_data(
|
|||
if not HAS_VTK:
|
||||
warnings.warn("VTK not supported", stacklevel=2)
|
||||
|
||||
if obj.wrapped is None:
|
||||
if not obj:
|
||||
raise ValueError("Cannot convert an empty shape")
|
||||
|
||||
vtk_shape = IVtkOCC_Shape(obj.wrapped)
|
||||
|
|
|
|||
|
|
@ -183,6 +183,59 @@ class BuildLineTests(unittest.TestCase):
|
|||
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
||||
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p = FilletPolyline(
|
||||
(0, 0),
|
||||
(10, 0),
|
||||
(10, 10),
|
||||
(0, 10),
|
||||
radius=(1, 2, 3, 0),
|
||||
close=True,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p = FilletPolyline(
|
||||
(0, 0),
|
||||
(10, 0),
|
||||
(10, 10),
|
||||
(0, 10),
|
||||
radius=(1, 2, 3, 4),
|
||||
close=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p = FilletPolyline(
|
||||
(0, 0),
|
||||
(10, 0),
|
||||
(10, 10),
|
||||
(0, 10),
|
||||
radius=-1,
|
||||
close=True,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p = FilletPolyline(
|
||||
(0, 0),
|
||||
(10, 0),
|
||||
(10, 10),
|
||||
(0, 10),
|
||||
radius=(1, 2),
|
||||
close=True,
|
||||
)
|
||||
|
||||
with BuildLine(Plane.YZ):
|
||||
p = FilletPolyline(
|
||||
(0, 0),
|
||||
(10, 0),
|
||||
(10, 10),
|
||||
(0, 10),
|
||||
radius=(1, 2, 3, 4),
|
||||
close=True,
|
||||
)
|
||||
self.assertEqual(len(p.edges()), 8)
|
||||
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 4)
|
||||
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
|
||||
|
||||
with BuildLine(Plane.YZ):
|
||||
p = FilletPolyline(
|
||||
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
|
|||
distance=1, distance2=2, vertices=[vertex], edge=other_edge
|
||||
)
|
||||
|
||||
def test_make_rect(self):
|
||||
test_face = Face.make_plane()
|
||||
def test_plane_as_face(self):
|
||||
test_face = Face(Plane.XY)
|
||||
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
|
||||
|
||||
def test_length_width(self):
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ def test_shape_0d(obj, target, expected):
|
|||
run_test(obj, target, expected)
|
||||
|
||||
|
||||
# 1d Shapes
|
||||
ed1 = Line((0, 0), (5, 0)).edge()
|
||||
ed2 = Line((0, -1), (5, 1)).edge()
|
||||
ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
|
||||
|
|
@ -220,6 +221,195 @@ def test_shape_1d(obj, target, expected):
|
|||
run_test(obj, target, expected)
|
||||
|
||||
|
||||
# 2d Shapes
|
||||
fc1 = Rectangle(5, 5).face()
|
||||
fc2 = Pos(Z=5) * Rectangle(5, 5).face()
|
||||
fc3 = Rot(Y=90) * Rectangle(5, 5).face()
|
||||
fc4 = Rot(Z=45) * Rectangle(5, 5).face()
|
||||
fc5 = Pos(2.5, 2.5, 2.5) * Rot(0, 90) * Rectangle(5, 5).face()
|
||||
fc6 = Pos(2.5, 2.5) * Rot(0, 90, 45, Extrinsic.XYZ) * Rectangle(5, 5).face()
|
||||
fc7 = (Rot(90) * Cylinder(2, 4)).faces().filter_by(GeomType.CYLINDER)[0]
|
||||
|
||||
fc11 = Rectangle(4, 4).face()
|
||||
fc22 = sweep(Rot(90) * CenterArc((0, 0), 2, 0, 180), Line((0, 2), (0, -2)))
|
||||
sh1 = Shell([Pos(-4) * fc11, fc22])
|
||||
sh2 = Pos(Z=1) * sh1
|
||||
sh3 = Shell([Pos(-4) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
|
||||
sh4 = Shell([Pos(-4) * fc11, fc22, Pos(4) * fc11])
|
||||
sh5 = Pos(Z=1) * Shell([Pos(-2, 0, -2) * Rot(0, -90) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
|
||||
|
||||
shape_2d_matrix = [
|
||||
Case(fc1, vl2, None, "non-coincident", None),
|
||||
Case(fc1, vl1, [Vertex], "coincident", None),
|
||||
|
||||
Case(fc1, lc2, None, "non-coincident", None),
|
||||
Case(fc1, lc1, [Vertex], "coincident", None),
|
||||
|
||||
Case(fc2, ax1, None, "parallel/skew", None),
|
||||
Case(fc3, ax1, [Vertex], "intersecting", None),
|
||||
Case(fc1, ax1, [Edge], "collinear", None),
|
||||
|
||||
Case(fc1, pl3, None, "parallel/skew", None),
|
||||
Case(fc1, pl1, [Edge], "intersecting", None),
|
||||
Case(fc1, pl2, [Face], "collinear", None),
|
||||
Case(fc7, pl1, [Edge, Edge], "multi intersect", None),
|
||||
|
||||
Case(fc1, vt2, None, "non-coincident", None),
|
||||
Case(fc1, vt1, [Vertex], "coincident", None),
|
||||
|
||||
Case(fc1, ed3, None, "parallel/skew", None),
|
||||
Case(Pos(1) * fc3, ed1, [Vertex], "intersecting", None),
|
||||
Case(fc1, ed1, [Edge], "collinear", None),
|
||||
Case(Pos(1.1) * fc3, ed4, [Vertex, Vertex], "multi intersect", None),
|
||||
|
||||
Case(fc1, wi6, None, "parallel/skew", None),
|
||||
Case(Pos(1) * fc3, wi4, [Vertex], "intersecting", None),
|
||||
Case(fc1, wi1, [Edge, Edge], "2 collinear", None),
|
||||
Case(Rot(90) * fc4, wi5, [Vertex, Vertex], "multi intersect", None),
|
||||
Case(Rot(90) * fc4, wi2, [Vertex, Edge], "intersect + collinear", None),
|
||||
|
||||
Case(fc1, fc2, None, "parallel/skew", None),
|
||||
Case(fc1, fc3, [Edge], "intersecting", None),
|
||||
Case(fc1, fc4, [Face], "coplanar", None),
|
||||
Case(fc1, fc5, [Edge], "intersecting edge", None),
|
||||
Case(fc1, fc6, [Vertex], "intersecting vertex", None),
|
||||
Case(fc1, fc7, [Edge, Edge], "multi-intersecting", None),
|
||||
Case(fc7, Pos(Y=2) * fc7, [Face], "cyl intersecting", None),
|
||||
|
||||
Case(sh2, fc1, None, "parallel/skew", None),
|
||||
Case(Pos(Z=1) * sh3, fc1, [Edge], "intersecting", None),
|
||||
Case(sh1, fc1, [Face, Edge], "coplanar + intersecting", None),
|
||||
Case(sh4, fc1, [Face, Face], "2 coplanar", None),
|
||||
Case(sh5, fc1, [Edge, Edge], "2 intersecting", None),
|
||||
|
||||
Case(fc1, [fc4, Pos(2, 2) * fc1], [Face], "multi to_intersect, intersecting", None),
|
||||
Case(fc1, [ed1, Pos(2.5, 2.5) * fc1], [Edge], "multi to_intersect, intersecting", None),
|
||||
Case(fc7, [wi5, fc1], [Vertex], "multi to_intersect, intersecting", None),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("obj, target, expected", make_params(shape_2d_matrix))
|
||||
def test_shape_2d(obj, target, expected):
|
||||
run_test(obj, target, expected)
|
||||
|
||||
# 3d Shapes
|
||||
sl1 = Box(2, 2, 2).solid()
|
||||
sl2 = Pos(Z=5) * Box(2, 2, 2).solid()
|
||||
sl3 = Cylinder(2, 1).solid() - Cylinder(1.5, 1).solid()
|
||||
|
||||
wi7 = Wire([l1 := sl3.faces().sort_by(Axis.Z)[-1].edges()[0].trim(.3, .4),
|
||||
l2 := l1.trim(2, 3),
|
||||
RadiusArc(l1 @ 1, l2 @ 0, 1, short_sagitta=False)
|
||||
])
|
||||
|
||||
shape_3d_matrix = [
|
||||
Case(sl2, vl1, None, "non-coincident", None),
|
||||
Case(Pos(2) * sl1, vl1, [Vertex], "contained", None),
|
||||
Case(Pos(1, 1, -1) * sl1, vl1, [Vertex], "coincident", None),
|
||||
|
||||
Case(sl2, lc1, None, "non-coincident", None),
|
||||
Case(Pos(2) * sl1, lc1, [Vertex], "contained", None),
|
||||
Case(Pos(1, 1, -1) * sl1, lc1, [Vertex], "coincident", None),
|
||||
|
||||
Case(sl2, ax1, None, "non-coincident", None),
|
||||
Case(sl1, ax1, [Edge], "intersecting", None),
|
||||
Case(Pos(1, 1, 1) * sl1, ax2, [Edge], "coincident", None),
|
||||
|
||||
Case(sl1, pl3, None, "non-coincident", None),
|
||||
Case(sl1, pl2, [Face], "intersecting", None),
|
||||
|
||||
Case(sl2, vt1, None, "non-coincident", None),
|
||||
Case(Pos(2) * sl1, vt1, [Vertex], "contained", None),
|
||||
Case(Pos(1, 1, -1) * sl1, vt1, [Vertex], "coincident", None),
|
||||
|
||||
Case(sl1, ed3, None, "non-coincident", None),
|
||||
Case(sl1, ed1, [Edge], "intersecting", None),
|
||||
Case(sl1, Pos(0, 1, 1) * ed1, [Edge], "edge collinear", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
|
||||
Case(sl1, Pos(1, 1, 1) * ed1, [Vertex], "corner coincident", None),
|
||||
Case(Pos(2.1, 1) * sl1, ed4, [Edge, Edge], "multi-intersect", None),
|
||||
|
||||
Case(Pos(2, .5, -1) * sl1, wi6, None, "non-coincident", None),
|
||||
Case(Pos(2, .5, 1) * sl1, wi6, [Edge, Edge], "multi-intersecting", None),
|
||||
Case(sl3, wi7, [Edge, Edge], "multi-coincident, is_equal check", None),
|
||||
|
||||
Case(sl2, fc1, None, "non-coincident", None),
|
||||
Case(sl1, fc1, [Face], "intersecting", None),
|
||||
Case(Pos(3.5, 0, 1) * sl1, fc1, [Edge], "edge collinear", None),
|
||||
Case(Pos(3.5, 3.5) * sl1, fc1, [Vertex], "corner coincident", None),
|
||||
Case(Pos(.9) * sl1, fc7, [Face, Face], "multi-intersecting", None),
|
||||
|
||||
Case(sl2, sh1, None, "non-coincident", None),
|
||||
Case(Pos(-2) * sl1, sh1, [Face, Face], "multi-intersecting", None),
|
||||
|
||||
Case(sl1, sl2, None, "non-coincident", None),
|
||||
Case(sl1, Pos(1, 1, 1) * sl1, [Solid], "intersecting", None),
|
||||
Case(sl1, Pos(2, 2, 1) * sl1, [Edge], "edge collinear", None),
|
||||
Case(sl1, Pos(2, 2, 2) * sl1, [Vertex], "corner coincident", None),
|
||||
Case(sl1, Pos(.45) * sl3, [Solid, Solid], "multi-intersect", None),
|
||||
|
||||
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(.5, .5) * sl1], [Solid], "multi to_intersect, intersecting", None),
|
||||
Case(Pos(1.5, 1.5) * sl1, [sl3, Pos(Z=.5) * fc1], [Face], "multi to_intersect, intersecting", None),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("obj, target, expected", make_params(shape_3d_matrix))
|
||||
def test_shape_3d(obj, target, expected):
|
||||
run_test(obj, target, expected)
|
||||
|
||||
# Compound Shapes
|
||||
cp1 = Compound(GridLocations(5, 0, 2, 1) * Vertex())
|
||||
cp2 = Compound(GridLocations(5, 0, 2, 1) * Line((0, -1), (0, 1)))
|
||||
cp3 = Compound(GridLocations(5, 0, 2, 1) * Rectangle(2, 2))
|
||||
cp4 = Compound(GridLocations(5, 0, 2, 1) * Box(2, 2, 2))
|
||||
|
||||
cv1 = Curve() + [ed1, ed2, ed3]
|
||||
sk1 = Sketch() + [fc1, fc2, fc3]
|
||||
pt1 = Part() + [sl1, sl2, sl3]
|
||||
|
||||
|
||||
shape_compound_matrix = [
|
||||
Case(cp1, vl1, None, "non-coincident", None),
|
||||
Case(Pos(-.5) * cp1, vl1, [Vertex], "intersecting", None),
|
||||
|
||||
Case(cp2, lc1, None, "non-coincident", None),
|
||||
Case(Pos(-.5) * cp2, lc1, [Vertex], "intersecting", None),
|
||||
|
||||
Case(Pos(Z=1) * cp3, ax1, None, "non-coincident", None),
|
||||
Case(cp3, ax1, [Edge, Edge], "intersecting", None),
|
||||
|
||||
Case(Pos(Z=3) * cp4, pl2, None, "non-coincident", None),
|
||||
Case(cp4, pl2, [Face, Face], "intersecting", None),
|
||||
|
||||
Case(cp1, vt1, None, "non-coincident", None),
|
||||
Case(Pos(-.5) * cp1, vt1, [Vertex], "intersecting", None),
|
||||
|
||||
Case(Pos(Z=1) * cp2, ed1, None, "non-coincident", None),
|
||||
Case(cp2, ed1, [Vertex], "intersecting", None),
|
||||
|
||||
Case(Pos(Z=1) * cp3, fc1, None, "non-coincident", None),
|
||||
Case(cp3, fc1, [Face, Face], "intersecting", None),
|
||||
|
||||
Case(Pos(Z=5) * cp4, sl1, None, "non-coincident", None),
|
||||
Case(Pos(2) * cp4, sl1, [Solid], "intersecting", None),
|
||||
|
||||
Case(cp1, Pos(Z=1) * cp1, None, "non-coincident", None),
|
||||
Case(cp1, cp2, [Vertex, Vertex], "intersecting", None),
|
||||
Case(cp2, cp3, [Edge, Edge], "intersecting", None),
|
||||
Case(cp3, cp4, [Face, Face], "intersecting", None),
|
||||
|
||||
Case(cp1, Compound(children=cp1.get_type(Vertex)), [Vertex, Vertex], "mixed child type", None),
|
||||
Case(cp4, Compound(children=cp3.get_type(Face)), [Face, Face], "mixed child type", None),
|
||||
|
||||
Case(cp2, [cp3, cp4], [Edge, Edge], "multi to_intersect, intersecting", None),
|
||||
|
||||
Case(cv1, cp3, [Edge, Edge], "intersecting", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
|
||||
Case(sk1, cp3, [Face, Face], "intersecting", None),
|
||||
Case(pt1, cp3, [Face, Face], "intersecting", None),
|
||||
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("obj, target, expected", make_params(shape_compound_matrix))
|
||||
def test_shape_compound(obj, target, expected):
|
||||
run_test(obj, target, expected)
|
||||
|
||||
# FreeCAD issue example
|
||||
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
||||
c2 = CenterArc((19, 0), 10, 0, 360).edge()
|
||||
|
|
@ -240,7 +430,7 @@ freecad_matrix = [
|
|||
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
|
||||
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
|
||||
|
||||
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", "Should return 2 Vertices"),
|
||||
Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", None),
|
||||
Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
|
||||
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
||||
Case(c1, vert, [Vertex], "circle, vert, tangent", None),
|
||||
|
|
@ -263,11 +453,11 @@ w1 = Wire.make_circle(0.5)
|
|||
f1 = Face(Wire.make_circle(0.5))
|
||||
|
||||
issues_matrix = [
|
||||
Case(t, t, [Face, Face], "issue #1015", "Returns Compound"),
|
||||
Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"),
|
||||
Case(a, b, [Edge], "issue #918", "Returns empty Compound"),
|
||||
Case(e1, w1, [Vertex, Vertex], "issue #697"),
|
||||
Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"),
|
||||
Case(t, t, [Face, Face], "issue #1015", None),
|
||||
Case(l, s, [Edge], "issue #945", None),
|
||||
Case(a, b, [Edge], "issue #918", None),
|
||||
Case(e1, w1, [Vertex, Vertex], "issue #697", None),
|
||||
Case(e1, f1, [Edge], "issue #697", None),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
||||
|
|
@ -279,6 +469,9 @@ def test_issues(obj, target, expected):
|
|||
exception_matrix = [
|
||||
Case(vt1, Color(), None, "Unsupported type", None),
|
||||
Case(ed1, Color(), None, "Unsupported type", None),
|
||||
Case(fc1, Color(), None, "Unsupported type", None),
|
||||
Case(sl1, Color(), None, "Unsupported type", None),
|
||||
Case(cp1, Color(), None, "Unsupported type", None),
|
||||
]
|
||||
|
||||
@pytest.mark.skip
|
||||
|
|
|
|||
|
|
@ -229,13 +229,15 @@ class TestOrientedBoundBox(unittest.TestCase):
|
|||
obb = OrientedBoundBox(rect)
|
||||
corners = obb.corners
|
||||
poly = Polygon(*corners, align=None)
|
||||
self.assertAlmostEqual(rect.intersect(poly).area, rect.area, 5)
|
||||
area = sum(f.area for f in rect.intersect(poly).faces())
|
||||
self.assertAlmostEqual(area, rect.area, 5)
|
||||
|
||||
for face in Box(1, 2, 3).faces():
|
||||
obb = OrientedBoundBox(face)
|
||||
corners = obb.corners
|
||||
poly = Polygon(*corners, align=None)
|
||||
self.assertAlmostEqual(face.intersect(poly).area, face.area, 5)
|
||||
area = sum(f.area for f in face.intersect(poly).faces())
|
||||
self.assertAlmostEqual(area, face.area, 5)
|
||||
|
||||
def test_line_corners(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -299,7 +299,8 @@ class TestShape(unittest.TestCase):
|
|||
predicted_location = Location(offset) * Rotation(*rotation)
|
||||
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
||||
intersect = shape.intersect(located_shape)
|
||||
self.assertAlmostEqual(intersect.volume, 1, 5)
|
||||
volume = sum(s.volume for s in intersect.solids())
|
||||
self.assertAlmostEqual(volume, 1, 5)
|
||||
|
||||
def test_position_and_orientation(self):
|
||||
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
||||
|
|
@ -475,7 +476,7 @@ class TestShape(unittest.TestCase):
|
|||
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
||||
self.assertListEqual(edges, [])
|
||||
|
||||
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY))
|
||||
verts, edges = Vertex(1, 2, 0)._ocp_section(Face(Plane.XY))
|
||||
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
||||
self.assertListEqual(edges, [])
|
||||
|
||||
|
|
@ -493,7 +494,7 @@ class TestShape(unittest.TestCase):
|
|||
self.assertEqual(len(edges1), 1)
|
||||
self.assertAlmostEqual(edges1[0].length, 20, 5)
|
||||
|
||||
vertices2, edges2 = cylinder._ocp_section(Face.make_plane(pln))
|
||||
vertices2, edges2 = cylinder._ocp_section(Face(pln))
|
||||
self.assertEqual(len(vertices2), 1)
|
||||
self.assertEqual(len(edges2), 1)
|
||||
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
|
||||
|
|
@ -588,7 +589,7 @@ class TestShape(unittest.TestCase):
|
|||
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
||||
with self.assertRaises(ValueError):
|
||||
empty.distance_to(Vector(1, 1, 1))
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(AttributeError):
|
||||
box.intersect(empty_loc)
|
||||
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
||||
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
||||
|
|
|
|||
|
|
@ -153,7 +153,9 @@ class TestSolid(unittest.TestCase):
|
|||
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
||||
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
|
||||
intersect = top.translate((0, 0, -1)).intersect(bottom)
|
||||
area = sum(f.area for f in intersect.faces())
|
||||
self.assertAlmostEqual(area, 1, 5)
|
||||
# Wire
|
||||
base = Wire.make_rect(1, 1)
|
||||
twist = Solid.extrude_linear_with_rotation(
|
||||
|
|
@ -162,7 +164,9 @@ class TestSolid(unittest.TestCase):
|
|||
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
||||
self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
|
||||
intersect = top.translate((0, 0, -1)).intersect(bottom)
|
||||
area = sum(f.area for f in intersect.faces())
|
||||
self.assertAlmostEqual(area, 1, 5)
|
||||
|
||||
def test_make_loft(self):
|
||||
loft = Solid.make_loft(
|
||||
|
|
|
|||
|
|
@ -231,7 +231,8 @@ class DimensionLineTestCase(unittest.TestCase):
|
|||
],
|
||||
draft=metric,
|
||||
)
|
||||
self.assertGreater(hole.intersect(d_line).area, 0)
|
||||
area = sum(f.area for f in hole.intersect(d_line).faces())
|
||||
self.assertGreater(area, 0)
|
||||
|
||||
def test_outside_arrows(self):
|
||||
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue