mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Compare commits
87 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a971cbbad6 | ||
|
|
726a72a20b | ||
|
|
3871345dcd | ||
|
|
6605b676a3 | ||
|
|
17ccdd01cc | ||
|
|
3474dc61d2 | ||
|
|
5adf296fd8 | ||
|
|
8985220c79 | ||
|
|
e7045ea856 | ||
|
|
2fa0dd22da | ||
|
|
ad77bf5f7f | ||
|
|
0bedc9c9ad | ||
|
|
a8fc16b344 | ||
|
|
6eb11ad9f6 | ||
|
|
05eb8fbd4d | ||
|
|
82aa0aa367 | ||
|
|
2d82b2ca5c | ||
|
|
7f6d44249b | ||
|
|
bdad339e58 | ||
|
|
bc8d01dc7e | ||
|
|
7a4f1f7e55 | ||
|
|
70764bbe08 | ||
|
|
26caed754c | ||
|
|
02a8c07e0a | ||
|
|
607efade27 | ||
|
|
a5e95fe72f | ||
|
|
e6d272b2fa | ||
|
|
a00cecbc38 | ||
|
|
4507d78fff | ||
|
|
f3b080e351 | ||
|
|
bc96e84dc2 | ||
|
|
8980120cb2 | ||
|
|
f144ca5aa8 | ||
|
|
7f4e92f0bf | ||
|
|
9707749c61 | ||
|
|
d329cf1094 | ||
|
|
837b743a13 | ||
|
|
caa25671fb | ||
|
|
1095f3ee4c | ||
|
|
c7034202f3 | ||
|
|
dc90a4b15a | ||
|
|
e92255cefc | ||
|
|
173c7b08e2 | ||
|
|
2768427087 | ||
|
|
df17ae8698 | ||
|
|
5f67a1932a | ||
|
|
5ea2dab174 | ||
|
|
5523a2184c | ||
|
|
c384df21c7 | ||
|
|
68f6ef2125 | ||
|
|
3877fd5876 | ||
|
|
6937501e79 | ||
|
|
20854b3d4d | ||
|
|
083cb1611c | ||
|
|
cc34b5a743 | ||
|
|
5d84002aa5 | ||
|
|
38e69844b3 | ||
|
|
e6d98de840 | ||
|
|
395ecc173e | ||
|
|
513c50530c | ||
|
|
0416967a61 | ||
|
|
3bea4d3228 | ||
|
|
27567a10ef | ||
|
|
b049e6a8ce | ||
|
|
3713574519 | ||
|
|
5d7b098379 | ||
|
|
069b691964 | ||
|
|
315605f485 | ||
|
|
c13ef47cef | ||
|
|
a7b554001f | ||
|
|
cfd4546585 | ||
|
|
89dedd0888 | ||
|
|
8c32e3bed3 | ||
|
|
c83aedaae2 | ||
|
|
9a6c382ced | ||
|
|
fb324adced | ||
|
|
6ce4a31355 | ||
|
|
a6d8f9bdc1 | ||
|
|
0013b9fa87 | ||
|
|
5d485ee705 | ||
|
|
c7bf48c80c | ||
|
|
99da8912df | ||
|
|
640b530058 | ||
|
|
bb9495a821 | ||
|
|
f4c79db263 | ||
|
|
3d8bbcc539 | ||
|
|
335f82d740 |
76 changed files with 2887 additions and 1271 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -41,3 +41,6 @@ venv.bak/
|
||||||
|
|
||||||
# Profiling debris.
|
# Profiling debris.
|
||||||
prof/
|
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.
|
are happy with your code.
|
||||||
|
|
||||||
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
|
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
|
||||||
- Install development dependencies: `pip install -e .[development]`
|
- Install development dependencies: `pip install -e ".[development]"`
|
||||||
- Install docs dependencies: `pip install -e .[docs]`
|
- Install docs dependencies: `pip install -e ".[docs]"`
|
||||||
- Install `build123d` in editable mode from current dir: `pip install -e .`
|
- Install `build123d` in editable mode from current dir: `pip install -e .`
|
||||||
- Run tests with: `python -m pytest -n auto`
|
- Run tests with: `python -m pytest -n auto`
|
||||||
- Build docs with: `cd docs && make html`
|
- Build docs with: `cd docs && make html`
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ build123d of a piece of angle iron:
|
||||||
|
|
||||||
**build123d Approach**
|
**build123d Approach**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# Builder mode
|
# Builder mode
|
||||||
with BuildPart() as angle_iron:
|
with BuildPart() as angle_iron:
|
||||||
|
|
@ -135,7 +135,7 @@ build123d of a piece of angle iron:
|
||||||
fillet(angle_iron.edges().filter_by(lambda e: e.is_interior), 5 * MM)
|
fillet(angle_iron.edges().filter_by(lambda e: e.is_interior), 5 * MM)
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# Algebra mode
|
# Algebra mode
|
||||||
profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN)
|
profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ python context manager.
|
||||||
...
|
...
|
||||||
)
|
)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# build123d API
|
# build123d API
|
||||||
with BuildPart() as pillow_block:
|
with BuildPart() as pillow_block:
|
||||||
|
|
@ -43,7 +43,7 @@ Each object and operation is now a class instantiation that interacts with the
|
||||||
active context implicitly for the user. These instantiations can be assigned to
|
active context implicitly for the user. These instantiations can be assigned to
|
||||||
an instance variable as with standard python programming for direct use.
|
an instance variable as with standard python programming for direct use.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch() as plan:
|
with BuildSketch() as plan:
|
||||||
r = Rectangle(width, height)
|
r = Rectangle(width, height)
|
||||||
|
|
@ -62,7 +62,7 @@ with tangents equal to the tangents of l5 and l6 at their end and beginning resp
|
||||||
Being able to extract information from existing features allows the user to "snap" new
|
Being able to extract information from existing features allows the user to "snap" new
|
||||||
features to these points without knowing their numeric values.
|
features to these points without knowing their numeric values.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildLine() as outline:
|
with BuildLine() as outline:
|
||||||
...
|
...
|
||||||
|
|
@ -81,6 +81,7 @@ by the last operation and fillets them. Such a selection would be quite difficul
|
||||||
otherwise.
|
otherwise.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/intersecting_pipes.py
|
.. literalinclude:: ../examples/intersecting_pipes.py
|
||||||
|
:language: build123d
|
||||||
:lines: 30, 39-49
|
:lines: 30, 39-49
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,7 +105,7 @@ sorting which opens up the full functionality of python lists. To aid the
|
||||||
user, common operations have been optimized as shown here along with
|
user, common operations have been optimized as shown here along with
|
||||||
a fully custom selection:
|
a fully custom selection:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
top = rail.faces().filter_by(Axis.Z)[-1]
|
top = rail.faces().filter_by(Axis.Z)[-1]
|
||||||
...
|
...
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Creating lots of Shapes in a loop means for every step ``fuse`` and ``clean`` wi
|
||||||
In an example like the below, both functions get slower and slower the more objects are
|
In an example like the below, both functions get slower and slower the more objects are
|
||||||
already fused. Overall it takes on an M1 Mac 4.76 sec.
|
already fused. Overall it takes on an M1 Mac 4.76 sec.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
diam = 80
|
diam = 80
|
||||||
holes = Sketch()
|
holes = Sketch()
|
||||||
|
|
@ -22,7 +22,7 @@ already fused. Overall it takes on an M1 Mac 4.76 sec.
|
||||||
One way to avoid it is to use lazy evaluation for the algebra operations. Just collect all objects and
|
One way to avoid it is to use lazy evaluation for the algebra operations. Just collect all objects and
|
||||||
then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it takes 0.19 sec.
|
then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it takes 0.19 sec.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
r = Rectangle(2, 2)
|
r = Rectangle(2, 2)
|
||||||
holes = [
|
holes = [
|
||||||
|
|
@ -36,7 +36,7 @@ then call ``fuse`` (``+``) once with all objects and ``clean`` once. Overall it
|
||||||
Another way to leverage the vectorized algebra operations is to add a list comprehension of objects to
|
Another way to leverage the vectorized algebra operations is to add a list comprehension of objects to
|
||||||
an empty ``Part``, ``Sketch`` or ``Curve``:
|
an empty ``Part``, ``Sketch`` or ``Curve``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
polygons = Sketch() + [
|
polygons = Sketch() + [
|
||||||
loc * RegularPolygon(radius=5, side_count=5)
|
loc * RegularPolygon(radius=5, side_count=5)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Here we'll assign labels to all of the components that will be part of the box
|
||||||
assembly:
|
assembly:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Add labels]
|
:start-after: [Add labels]
|
||||||
:end-before: [Create assembly]
|
:end-before: [Create assembly]
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ Creation of the assembly is done by simply creating a :class:`~topology.Compound
|
||||||
appropriate ``parent`` and ``children`` attributes as shown here:
|
appropriate ``parent`` and ``children`` attributes as shown here:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Create assembly]
|
:start-after: [Create assembly]
|
||||||
:end-before: [Display assembly]
|
:end-before: [Display assembly]
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ To display the topology of an assembly :class:`~topology.Compound`, the :meth:`~
|
||||||
method can be used as follows:
|
method can be used as follows:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Display assembly]
|
:start-after: [Display assembly]
|
||||||
:end-before: [Add to the assembly by assigning the parent attribute of an object]
|
:end-before: [Add to the assembly by assigning the parent attribute of an object]
|
||||||
|
|
||||||
|
|
@ -59,6 +62,7 @@ which results in:
|
||||||
To add to an assembly :class:`~topology.Compound` one can change either ``children`` or ``parent`` attributes.
|
To add to an assembly :class:`~topology.Compound` one can change either ``children`` or ``parent`` attributes.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Add to the assembly by assigning the parent attribute of an object]
|
:start-after: [Add to the assembly by assigning the parent attribute of an object]
|
||||||
:end-before: [Check that the components in the assembly don't intersect]
|
:end-before: [Check that the components in the assembly don't intersect]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ with BuildSketch(Location((0, -r1, y3))) as sk_body:
|
||||||
m3 = IntersectingLine(m2 @ 1, m2 % 1, c1)
|
m3 = IntersectingLine(m2 @ 1, m2 % 1, c1)
|
||||||
m4 = Line(m3 @ 1, (r1, r1))
|
m4 = Line(m3 @ 1, (r1, r1))
|
||||||
m5 = JernArc(m4 @ 1, m4 % 1, r1, -90)
|
m5 = JernArc(m4 @ 1, m4 % 1, r1, -90)
|
||||||
m6 = Line(m5 @ 1, m1 @ 0)
|
mirror(about=Plane.YZ)
|
||||||
mirror(make_face(l.line), Plane.YZ)
|
make_face()
|
||||||
fillet(sk_body.vertices().group_by(Axis.Y)[1], 12)
|
fillet(sk_body.vertices().group_by(Axis.Y)[1], 12)
|
||||||
with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)):
|
with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)):
|
||||||
Circle(r2, mode=Mode.SUBTRACT)
|
Circle(r2, mode=Mode.SUBTRACT)
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,17 @@ with BuildPart() as ppp109:
|
||||||
split(bisect_by=Plane.YZ)
|
split(bisect_by=Plane.YZ)
|
||||||
extrude(amount=6)
|
extrude(amount=6)
|
||||||
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
f = ppp109.faces().filter_by(Axis((0, 0, 0), (-1, 0, 1)))[0]
|
||||||
# extrude(f, until=Until.NEXT) # throws a warning
|
extrude(f, until=Until.NEXT)
|
||||||
extrude(f, amount=10)
|
fillet(ppp109.edges().filter_by(Axis.Y).sort_by(Axis.Z)[2], 16)
|
||||||
fillet(ppp109.edge(Select.NEW), 16)
|
# extrude(f, amount=10)
|
||||||
|
# fillet(ppp109.edges(Select.NEW), 16)
|
||||||
|
|
||||||
|
|
||||||
show(ppp109)
|
show(ppp109)
|
||||||
|
|
||||||
got_mass = ppp109.part.volume*densb
|
got_mass = ppp109.part.volume * densb
|
||||||
want_mass = 307.23
|
want_mass = 307.23
|
||||||
tolerance = 1
|
tolerance = 1
|
||||||
delta = abs(got_mass - want_mass)
|
delta = abs(got_mass - want_mass)
|
||||||
print(f"Mass: {got_mass:0.2f} g")
|
print(f"Mass: {got_mass:0.2f} g")
|
||||||
assert delta < tolerance, f'{got_mass=}, {want_mass=}, {delta=}, {tolerance=}'
|
assert delta < tolerance, f"{got_mass=}, {want_mass=}, {delta=}, {tolerance=}"
|
||||||
|
|
|
||||||
75
docs/build123d_lexer.py
Normal file
75
docs/build123d_lexer.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import inspect
|
||||||
|
import enum
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pygments.lexers.python import PythonLexer
|
||||||
|
from pygments.token import Name
|
||||||
|
from sphinx.highlighting import lexers
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
|
||||||
|
import build123d
|
||||||
|
|
||||||
|
|
||||||
|
class Build123dLexer(PythonLexer):
|
||||||
|
"""
|
||||||
|
Python lexer extended with Build123d-specific highlighting.
|
||||||
|
Dynamically pulls symbols from build123d.__all__.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXTRA_SYMBOLS = set(getattr(build123d, "__all__", []))
|
||||||
|
|
||||||
|
EXTRA_CLASSES = {
|
||||||
|
n for n in EXTRA_SYMBOLS
|
||||||
|
if n[0].isupper()
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTRA_CONSTANTS = {
|
||||||
|
n for n in EXTRA_SYMBOLS
|
||||||
|
if n.isupper() and not callable(getattr(build123d, n, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTRA_ENUMS = {
|
||||||
|
n for n in EXTRA_SYMBOLS
|
||||||
|
if inspect.isclass(getattr(build123d, n, None)) and issubclass(getattr(build123d, n), enum.Enum)
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTRA_FUNCTIONS = EXTRA_SYMBOLS - EXTRA_CLASSES - EXTRA_CONSTANTS - EXTRA_ENUMS
|
||||||
|
|
||||||
|
def get_tokens_unprocessed(self, text):
|
||||||
|
"""
|
||||||
|
Yield tokens, highlighting Build123d symbols, including chained accesses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dot_chain = False
|
||||||
|
for index, token, value in super().get_tokens_unprocessed(text):
|
||||||
|
if value == ".":
|
||||||
|
dot_chain = True
|
||||||
|
yield index, token, value
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dot_chain:
|
||||||
|
# In a chain, don't use top-level categories
|
||||||
|
if value[0].isupper():
|
||||||
|
yield index, Name.Class, value
|
||||||
|
elif value.isupper():
|
||||||
|
yield index, Name.Constant, value
|
||||||
|
else:
|
||||||
|
yield index, Name.Function, value
|
||||||
|
dot_chain = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Top-level classification from __all__
|
||||||
|
if value in self.EXTRA_CLASSES:
|
||||||
|
yield index, Name.Class, value
|
||||||
|
elif value in self.EXTRA_FUNCTIONS:
|
||||||
|
yield index, Name.Function, value
|
||||||
|
elif value in self.EXTRA_CONSTANTS:
|
||||||
|
yield index, Name.Constant, value
|
||||||
|
elif value in self.EXTRA_ENUMS:
|
||||||
|
yield index, Name.Builtin, value
|
||||||
|
else:
|
||||||
|
yield index, token, value
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
lexers["build123d"] = Build123dLexer()
|
||||||
|
return {"version": "0.1"}
|
||||||
|
|
@ -15,6 +15,7 @@ Basic Functionality
|
||||||
The following is a simple BuildLine example:
|
The following is a simple BuildLine example:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 1]
|
:start-after: [Ex. 1]
|
||||||
:end-before: [Ex. 1]
|
:end-before: [Ex. 1]
|
||||||
|
|
||||||
|
|
@ -50,6 +51,7 @@ point ``(0,0)`` and ``(2,0)``. This can be improved upon by specifying
|
||||||
constraints that lock the arc to those two end points, as follows:
|
constraints that lock the arc to those two end points, as follows:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 2]
|
:start-after: [Ex. 2]
|
||||||
:end-before: [Ex. 2]
|
:end-before: [Ex. 2]
|
||||||
|
|
||||||
|
|
@ -63,6 +65,7 @@ This example can be improved on further by calculating the mid-point
|
||||||
of the arc as follows:
|
of the arc as follows:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 3]
|
:start-after: [Ex. 3]
|
||||||
:end-before: [Ex. 3]
|
:end-before: [Ex. 3]
|
||||||
|
|
||||||
|
|
@ -73,6 +76,7 @@ To make the design even more parametric, the height of the arc can be calculated
|
||||||
from ``l1`` as follows:
|
from ``l1`` as follows:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 4]
|
:start-after: [Ex. 4]
|
||||||
:end-before: [Ex. 4]
|
:end-before: [Ex. 4]
|
||||||
|
|
||||||
|
|
@ -87,6 +91,7 @@ The other operator that is commonly used within BuildLine is ``%`` the tangent a
|
||||||
operator. Here is another example:
|
operator. Here is another example:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 5]
|
:start-after: [Ex. 5]
|
||||||
:end-before: [Ex. 5]
|
:end-before: [Ex. 5]
|
||||||
|
|
||||||
|
|
@ -124,6 +129,7 @@ Here is an example of using BuildLine to create an object that otherwise might b
|
||||||
difficult to create:
|
difficult to create:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 6]
|
:start-after: [Ex. 6]
|
||||||
:end-before: [Ex. 6]
|
:end-before: [Ex. 6]
|
||||||
|
|
||||||
|
|
@ -155,6 +161,7 @@ The other primary reasons to use BuildLine is to create paths for BuildPart
|
||||||
define a path:
|
define a path:
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 7]
|
:start-after: [Ex. 7]
|
||||||
:end-before: [Ex. 7]
|
:end-before: [Ex. 7]
|
||||||
|
|
||||||
|
|
@ -184,6 +191,7 @@ to global coordinates. Sometimes it's convenient to work on another plane, espec
|
||||||
creating paths for BuildPart ``Sweep`` operations.
|
creating paths for BuildPart ``Sweep`` operations.
|
||||||
|
|
||||||
.. literalinclude:: objects_1d.py
|
.. literalinclude:: objects_1d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 8]
|
:start-after: [Ex. 8]
|
||||||
:end-before: [Ex. 8]
|
:end-before: [Ex. 8]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ Basic Functionality
|
||||||
The following is a simple BuildPart example:
|
The following is a simple BuildPart example:
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 2]
|
:start-after: [Ex. 2]
|
||||||
:end-before: [Ex. 2]
|
:end-before: [Ex. 2]
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ This tea cup example uses implicit parameters - note the :func:`~operations_gene
|
||||||
operation on the last line:
|
operation on the last line:
|
||||||
|
|
||||||
.. literalinclude:: ../examples/tea_cup.py
|
.. literalinclude:: ../examples/tea_cup.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:emphasize-lines: 52
|
:emphasize-lines: 52
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ Basic Functionality
|
||||||
The following is a simple BuildSketch example:
|
The following is a simple BuildSketch example:
|
||||||
|
|
||||||
.. literalinclude:: objects_2d.py
|
.. literalinclude:: objects_2d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 13]
|
:start-after: [Ex. 13]
|
||||||
:end-before: [Ex. 13]
|
:end-before: [Ex. 13]
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ As an example, let's build the following simple control box with a display on an
|
||||||
Here is the code:
|
Here is the code:
|
||||||
|
|
||||||
.. literalinclude:: objects_2d.py
|
.. literalinclude:: objects_2d.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 14]
|
:start-after: [Ex. 14]
|
||||||
:end-before: [Ex. 14]
|
:end-before: [Ex. 14]
|
||||||
:emphasize-lines: 14-25
|
:emphasize-lines: 14-25
|
||||||
|
|
@ -88,14 +90,14 @@ on ``Plane.XY`` which one can see by looking at the ``sketch_local`` property of
|
||||||
sketch. For example, to display the local version of the ``display`` sketch from
|
sketch. For example, to display the local version of the ``display`` sketch from
|
||||||
above, one would use:
|
above, one would use:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
show_object(display.sketch_local, name="sketch on Plane.XY")
|
show_object(display.sketch_local, name="sketch on Plane.XY")
|
||||||
|
|
||||||
while the sketches as applied to their target workplanes is accessible through
|
while the sketches as applied to their target workplanes is accessible through
|
||||||
the ``sketch`` property, as follows:
|
the ``sketch`` property, as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
show_object(display.sketch, name="sketch on target workplane(s)")
|
show_object(display.sketch, name="sketch on target workplane(s)")
|
||||||
|
|
||||||
|
|
@ -106,7 +108,7 @@ that the new Face may not be oriented as expected. To reorient the Face manually
|
||||||
to ``Plane.XY`` one can use the :meth:`~geometry.to_local_coords` method as
|
to ``Plane.XY`` one can use the :meth:`~geometry.to_local_coords` method as
|
||||||
follows:
|
follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
reoriented_face = plane.to_local_coords(face)
|
reoriented_face = plane.to_local_coords(face)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ extensions = [
|
||||||
"sphinx_design",
|
"sphinx_design",
|
||||||
"sphinx_copybutton",
|
"sphinx_copybutton",
|
||||||
"hoverxref.extension",
|
"hoverxref.extension",
|
||||||
|
"build123d_lexer"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Napoleon settings
|
# Napoleon settings
|
||||||
|
|
@ -99,6 +100,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
#
|
#
|
||||||
# html_theme = "alabaster"
|
# html_theme = "alabaster"
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
pygments_style = "colorful"
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ Sometimes the best debugging aid is just placing a print statement in your code.
|
||||||
of the build123d classes are setup to provide useful information beyond their class and
|
of the build123d classes are setup to provide useful information beyond their class and
|
||||||
location in memory, as follows:
|
location in memory, as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
plane = Plane.XY.offset(1)
|
plane = Plane.XY.offset(1)
|
||||||
print(f"{plane=}")
|
print(f"{plane=}")
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ modify it by replacing chimney with a BREP version.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/benchy.py
|
.. literalinclude:: ../examples/benchy.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -184,6 +185,7 @@ surface.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/bicycle_tire.py
|
.. literalinclude:: ../examples/bicycle_tire.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -204,12 +206,14 @@ The builder mode example also generates the SVG file `logo.svg`.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/build123d_logo.py
|
.. literalinclude:: ../examples/build123d_logo.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/build123d_logo_algebra.py
|
.. literalinclude:: ../examples/build123d_logo_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -228,6 +232,7 @@ using the `draft` operation to add appropriate draft angles for mold release.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/cast_bearing_unit.py
|
.. literalinclude:: ../examples/cast_bearing_unit.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -257,12 +262,14 @@ This example also demonstrates building complex lines that snap to existing feat
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/canadian_flag.py
|
.. literalinclude:: ../examples/canadian_flag.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/canadian_flag_algebra.py
|
.. literalinclude:: ../examples/canadian_flag_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -293,12 +300,14 @@ This example demonstrates placing holes around a part.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/circuit_board.py
|
.. literalinclude:: ../examples/circuit_board.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/circuit_board_algebra.py
|
.. literalinclude:: ../examples/circuit_board_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -313,12 +322,14 @@ Clock Face
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/clock.py
|
.. literalinclude:: ../examples/clock.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/clock_algebra.py
|
.. literalinclude:: ../examples/clock_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -340,6 +351,7 @@ Fast Grid Holes
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/fast_grid_holes.py
|
.. literalinclude:: ../examples/fast_grid_holes.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -367,12 +379,14 @@ Handle
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/handle.py
|
.. literalinclude:: ../examples/handle.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/handle_algebra.py
|
.. literalinclude:: ../examples/handle_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -388,12 +402,14 @@ Heat Exchanger
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/heat_exchanger.py
|
.. literalinclude:: ../examples/heat_exchanger.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/heat_exchanger_algebra.py
|
.. literalinclude:: ../examples/heat_exchanger_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -412,12 +428,14 @@ Key Cap
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/key_cap.py
|
.. literalinclude:: ../examples/key_cap.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/key_cap_algebra.py
|
.. literalinclude:: ../examples/key_cap_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -444,6 +462,7 @@ YouTube channel. There are two key features:
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/maker_coin.py
|
.. literalinclude:: ../examples/maker_coin.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -462,12 +481,14 @@ the top and bottom by type, and shelling.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/loft.py
|
.. literalinclude:: ../examples/loft.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/loft_algebra.py
|
.. literalinclude:: ../examples/loft_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -488,6 +509,7 @@ to aid 3D printing.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/pegboard_j_hook.py
|
.. literalinclude:: ../examples/pegboard_j_hook.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -495,6 +517,7 @@ to aid 3D printing.
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/pegboard_j_hook_algebra.py
|
.. literalinclude:: ../examples/pegboard_j_hook_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -521,6 +544,7 @@ embodying ideals of symmetry and balance.
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/platonic_solids.py
|
.. literalinclude:: ../examples/platonic_solids.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -539,6 +563,7 @@ imported as code from an SVG file and modified to the code found here.
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/playing_cards.py
|
.. literalinclude:: ../examples/playing_cards.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -558,6 +583,7 @@ are used to position all of objects.
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/stud_wall.py
|
.. literalinclude:: ../examples/stud_wall.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -571,12 +597,14 @@ Tea Cup
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/tea_cup.py
|
.. literalinclude:: ../examples/tea_cup.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/tea_cup_algebra.py
|
.. literalinclude:: ../examples/tea_cup_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -610,6 +638,7 @@ Toy Truck
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/toy_truck.py
|
.. literalinclude:: ../examples/toy_truck.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -630,12 +659,14 @@ Vase
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/vase.py
|
.. literalinclude:: ../examples/vase.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/vase_algebra.py
|
.. literalinclude:: ../examples/vase_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -677,11 +708,13 @@ selecting edges by position range and type for the application of fillets
|
||||||
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/boxes_on_faces.py
|
.. literalinclude:: ../examples/boxes_on_faces.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
|
||||||
|
|
||||||
.. literalinclude:: ../examples/boxes_on_faces_algebra.py
|
.. literalinclude:: ../examples/boxes_on_faces_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Methods and functions specific to exporting and importing build123d objects are
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as box_builder:
|
with BuildPart() as box_builder:
|
||||||
Box(1, 1, 1)
|
Box(1, 1, 1)
|
||||||
|
|
@ -142,7 +142,7 @@ The shapes generated from the above steps are to be added as shapes
|
||||||
in one of the exporters described below and written as either a DXF or SVG file as shown
|
in one of the exporters described below and written as either a DXF or SVG file as shown
|
||||||
in this example:
|
in this example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
view_port_origin=(-100, -50, 30)
|
view_port_origin=(-100, -50, 30)
|
||||||
visible, hidden = part.project_to_viewport(view_port_origin)
|
visible, hidden = part.project_to_viewport(view_port_origin)
|
||||||
|
|
@ -222,7 +222,7 @@ more complex API than the simple Shape exporters.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# Create the shapes and assign attributes
|
# Create the shapes and assign attributes
|
||||||
blue_shape = Solid.make_cone(20, 0, 50)
|
blue_shape = Solid.make_cone(20, 0, 50)
|
||||||
|
|
@ -276,7 +276,7 @@ Both 3MF and STL import (and export) are provided with the :class:`~mesher.Meshe
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
importer = Mesher()
|
importer = Mesher()
|
||||||
cone, cyl = importer.read("example.3mf")
|
cone, cyl = importer.read("example.3mf")
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,7 @@ expressive, algebraic modeling. It offers:
|
||||||
* Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``)
|
* Operator-driven modeling (``obj += sub_obj``, ``Plane.XZ * Pos(X=5) * Rectangle(1, 1)``)
|
||||||
for algebraic, readable, and composable design logic
|
for algebraic, readable, and composable design logic
|
||||||
|
|
||||||
The result is a framework that feels native to Python while providing the full power of
|
.. code-block:: build123d
|
||||||
OpenCascade geometry underneath.
|
|
||||||
|
|
||||||
|
|
||||||
With build123d, intricate parametric models can be created in just a few lines of readable
|
With build123d, intricate parametric models can be created in just a few lines of readable
|
||||||
|
|
@ -60,9 +59,10 @@ Python code—as demonstrated by the tea cup example below.
|
||||||
|
|
||||||
.. dropdown:: Teacup Example
|
.. dropdown:: Teacup Example
|
||||||
|
|
||||||
.. literalinclude:: ../examples/tea_cup.py
|
.. literalinclude:: ../examples/tea_cup.py
|
||||||
:start-after: [Code]
|
:language: build123d
|
||||||
:end-before: [End]
|
:start-after: [Code]
|
||||||
|
:end-before: [End]
|
||||||
|
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ Just about the simplest possible example, a rectangular :class:`~objects_part.Bo
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 1]
|
:start-after: [Ex. 1]
|
||||||
:end-before: [Ex. 1]
|
:end-before: [Ex. 1]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 1]
|
:start-after: [Ex. 1]
|
||||||
:end-before: [Ex. 1]
|
:end-before: [Ex. 1]
|
||||||
|
|
||||||
|
|
@ -63,6 +65,7 @@ A rectangular box, but with a hole added.
|
||||||
from the :class:`~objects_part.Box`.
|
from the :class:`~objects_part.Box`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 2]
|
:start-after: [Ex. 2]
|
||||||
:end-before: [Ex. 2]
|
:end-before: [Ex. 2]
|
||||||
|
|
||||||
|
|
@ -73,6 +76,7 @@ A rectangular box, but with a hole added.
|
||||||
from the :class:`~objects_part.Box`.
|
from the :class:`~objects_part.Box`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 2]
|
:start-after: [Ex. 2]
|
||||||
:end-before: [Ex. 2]
|
:end-before: [Ex. 2]
|
||||||
|
|
||||||
|
|
@ -94,6 +98,7 @@ Build a prismatic solid using extrusion.
|
||||||
and then use :class:`~build_part.BuildPart`'s :meth:`~operations_part.extrude` feature.
|
and then use :class:`~build_part.BuildPart`'s :meth:`~operations_part.extrude` feature.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 3]
|
:start-after: [Ex. 3]
|
||||||
:end-before: [Ex. 3]
|
:end-before: [Ex. 3]
|
||||||
|
|
||||||
|
|
@ -103,6 +108,7 @@ Build a prismatic solid using extrusion.
|
||||||
:class:`~objects_sketch.Rectangle`` and then use the :meth:`~operations_part.extrude` operation for parts.
|
:class:`~objects_sketch.Rectangle`` and then use the :meth:`~operations_part.extrude` operation for parts.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 3]
|
:start-after: [Ex. 3]
|
||||||
:end-before: [Ex. 3]
|
:end-before: [Ex. 3]
|
||||||
|
|
||||||
|
|
@ -126,6 +132,7 @@ variables for the line segments, but it will be useful in a later example.
|
||||||
from :class:`~build_line.BuildLine` into a closed Face.
|
from :class:`~build_line.BuildLine` into a closed Face.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 4]
|
:start-after: [Ex. 4]
|
||||||
:end-before: [Ex. 4]
|
:end-before: [Ex. 4]
|
||||||
|
|
||||||
|
|
@ -138,6 +145,7 @@ variables for the line segments, but it will be useful in a later example.
|
||||||
segments into a Face.
|
segments into a Face.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 4]
|
:start-after: [Ex. 4]
|
||||||
:end-before: [Ex. 4]
|
:end-before: [Ex. 4]
|
||||||
|
|
||||||
|
|
@ -158,6 +166,7 @@ Note that to build a closed face it requires line segments that form a closed sh
|
||||||
at one (or multiple) places.
|
at one (or multiple) places.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 5]
|
:start-after: [Ex. 5]
|
||||||
:end-before: [Ex. 5]
|
:end-before: [Ex. 5]
|
||||||
|
|
||||||
|
|
@ -168,6 +177,7 @@ Note that to build a closed face it requires line segments that form a closed sh
|
||||||
(with :class:`geometry.Rot`) would rotate the object.
|
(with :class:`geometry.Rot`) would rotate the object.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 5]
|
:start-after: [Ex. 5]
|
||||||
:end-before: [Ex. 5]
|
:end-before: [Ex. 5]
|
||||||
|
|
||||||
|
|
@ -188,6 +198,7 @@ Sometimes you need to create a number of features at various
|
||||||
You can use a list of points to construct multiple objects at once.
|
You can use a list of points to construct multiple objects at once.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 6]
|
:start-after: [Ex. 6]
|
||||||
:end-before: [Ex. 6]
|
:end-before: [Ex. 6]
|
||||||
|
|
||||||
|
|
@ -200,6 +211,7 @@ Sometimes you need to create a number of features at various
|
||||||
is short for ``obj - obj1 - obj2 - ob3`` (and more efficient, see :ref:`algebra_performance`).
|
is short for ``obj - obj1 - obj2 - ob3`` (and more efficient, see :ref:`algebra_performance`).
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 6]
|
:start-after: [Ex. 6]
|
||||||
:end-before: [Ex. 6]
|
:end-before: [Ex. 6]
|
||||||
|
|
||||||
|
|
@ -218,6 +230,7 @@ Sometimes you need to create a number of features at various
|
||||||
you would like.
|
you would like.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 7]
|
:start-after: [Ex. 7]
|
||||||
:end-before: [Ex. 7]
|
:end-before: [Ex. 7]
|
||||||
|
|
||||||
|
|
@ -227,6 +240,7 @@ Sometimes you need to create a number of features at various
|
||||||
for each location via loops or list comprehensions.
|
for each location via loops or list comprehensions.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 7]
|
:start-after: [Ex. 7]
|
||||||
:end-before: [Ex. 7]
|
:end-before: [Ex. 7]
|
||||||
|
|
||||||
|
|
@ -247,12 +261,14 @@ create the final profile.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 8]
|
:start-after: [Ex. 8]
|
||||||
:end-before: [Ex. 8]
|
:end-before: [Ex. 8]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 8]
|
:start-after: [Ex. 8]
|
||||||
:end-before: [Ex. 8]
|
:end-before: [Ex. 8]
|
||||||
|
|
||||||
|
|
@ -273,12 +289,14 @@ edges, you could simply pass in ``ex9.edges()``.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 9]
|
:start-after: [Ex. 9]
|
||||||
:end-before: [Ex. 9]
|
:end-before: [Ex. 9]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 9]
|
:start-after: [Ex. 9]
|
||||||
:end-before: [Ex. 9]
|
:end-before: [Ex. 9]
|
||||||
|
|
||||||
|
|
@ -303,6 +321,7 @@ be the highest z-dimension group.
|
||||||
makes use of :class:`~objects_part.Hole` which automatically cuts through the entire part.
|
makes use of :class:`~objects_part.Hole` which automatically cuts through the entire part.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 10]
|
:start-after: [Ex. 10]
|
||||||
:end-before: [Ex. 10]
|
:end-before: [Ex. 10]
|
||||||
|
|
||||||
|
|
@ -314,6 +333,7 @@ be the highest z-dimension group.
|
||||||
of :class:`~objects_part.Hole`. Different to the *context mode*, you have to add the ``depth`` of the whole.
|
of :class:`~objects_part.Hole`. Different to the *context mode*, you have to add the ``depth`` of the whole.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 10]
|
:start-after: [Ex. 10]
|
||||||
:end-before: [Ex. 10]
|
:end-before: [Ex. 10]
|
||||||
|
|
||||||
|
|
@ -339,6 +359,7 @@ be the highest z-dimension group.
|
||||||
cut these from the parent.
|
cut these from the parent.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 11]
|
:start-after: [Ex. 11]
|
||||||
:end-before: [Ex. 11]
|
:end-before: [Ex. 11]
|
||||||
|
|
||||||
|
|
@ -355,6 +376,7 @@ be the highest z-dimension group.
|
||||||
parent.
|
parent.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 11]
|
:start-after: [Ex. 11]
|
||||||
:end-before: [Ex. 11]
|
:end-before: [Ex. 11]
|
||||||
|
|
||||||
|
|
@ -376,12 +398,14 @@ edge that needs a complex profile.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 12]
|
:start-after: [Ex. 12]
|
||||||
:end-before: [Ex. 12]
|
:end-before: [Ex. 12]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 12]
|
:start-after: [Ex. 12]
|
||||||
:end-before: [Ex. 12]
|
:end-before: [Ex. 12]
|
||||||
|
|
||||||
|
|
@ -401,6 +425,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
|
||||||
We use a face to establish a location for :class:`~build_common.Locations`.
|
We use a face to establish a location for :class:`~build_common.Locations`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 13]
|
:start-after: [Ex. 13]
|
||||||
:end-before: [Ex. 13]
|
:end-before: [Ex. 13]
|
||||||
|
|
||||||
|
|
@ -410,6 +435,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
|
||||||
onto this plane.
|
onto this plane.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 13]
|
:start-after: [Ex. 13]
|
||||||
:end-before: [Ex. 13]
|
:end-before: [Ex. 13]
|
||||||
|
|
||||||
|
|
@ -417,7 +443,7 @@ Counter-sink and counter-bore holes are useful for creating recessed areas for f
|
||||||
|
|
||||||
.. _ex 14:
|
.. _ex 14:
|
||||||
|
|
||||||
14. Position on a line with '\@', '\%' and introduce Sweep
|
14. Position on a line with '\@', '\%' and introduce Sweep
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
|
|
||||||
build123d includes a feature for finding the position along a line segment. This
|
build123d includes a feature for finding the position along a line segment. This
|
||||||
|
|
@ -440,6 +466,7 @@ path, please see example 37 for a way to make this placement easier.
|
||||||
:meth:`~operations_part.revolve` requires a single connected wire.
|
:meth:`~operations_part.revolve` requires a single connected wire.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 14]
|
:start-after: [Ex. 14]
|
||||||
:end-before: [Ex. 14]
|
:end-before: [Ex. 14]
|
||||||
|
|
||||||
|
|
@ -449,6 +476,7 @@ path, please see example 37 for a way to make this placement easier.
|
||||||
path (in this case the path is taken from ``ex14_ln``).
|
path (in this case the path is taken from ``ex14_ln``).
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 14]
|
:start-after: [Ex. 14]
|
||||||
:end-before: [Ex. 14]
|
:end-before: [Ex. 14]
|
||||||
|
|
||||||
|
|
@ -471,6 +499,7 @@ Additionally the '@' operator is used to simplify the line segment commands.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 15]
|
:start-after: [Ex. 15]
|
||||||
:end-before: [Ex. 15]
|
:end-before: [Ex. 15]
|
||||||
|
|
||||||
|
|
@ -479,6 +508,7 @@ Additionally the '@' operator is used to simplify the line segment commands.
|
||||||
Combine lines via the pattern ``Curve() + [l1, l2, l3, l4, l5]``
|
Combine lines via the pattern ``Curve() + [l1, l2, l3, l4, l5]``
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 15]
|
:start-after: [Ex. 15]
|
||||||
:end-before: [Ex. 15]
|
:end-before: [Ex. 15]
|
||||||
|
|
||||||
|
|
@ -496,12 +526,14 @@ The ``Plane.offset()`` method shifts the plane in the normal direction (positive
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 16]
|
:start-after: [Ex. 16]
|
||||||
:end-before: [Ex. 16]
|
:end-before: [Ex. 16]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 16]
|
:start-after: [Ex. 16]
|
||||||
:end-before: [Ex. 16]
|
:end-before: [Ex. 16]
|
||||||
|
|
||||||
|
|
@ -520,12 +552,14 @@ Here we select the farthest face in the Y-direction and turn it into a :class:`~
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 17]
|
:start-after: [Ex. 17]
|
||||||
:end-before: [Ex. 17]
|
:end-before: [Ex. 17]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 17]
|
:start-after: [Ex. 17]
|
||||||
:end-before: [Ex. 17]
|
:end-before: [Ex. 17]
|
||||||
|
|
||||||
|
|
@ -546,6 +580,7 @@ with a negative distance.
|
||||||
We then use ``Mode.SUBTRACT`` to cut it out from the main body.
|
We then use ``Mode.SUBTRACT`` to cut it out from the main body.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 18]
|
:start-after: [Ex. 18]
|
||||||
:end-before: [Ex. 18]
|
:end-before: [Ex. 18]
|
||||||
|
|
||||||
|
|
@ -554,6 +589,7 @@ with a negative distance.
|
||||||
We then use ``-=`` to cut it out from the main body.
|
We then use ``-=`` to cut it out from the main body.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 18]
|
:start-after: [Ex. 18]
|
||||||
:end-before: [Ex. 18]
|
:end-before: [Ex. 18]
|
||||||
|
|
||||||
|
|
@ -578,6 +614,7 @@ this custom Axis.
|
||||||
:class:`~build_common.Locations` then the part would be offset from the workplane by the vertex z-position.
|
:class:`~build_common.Locations` then the part would be offset from the workplane by the vertex z-position.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 19]
|
:start-after: [Ex. 19]
|
||||||
:end-before: [Ex. 19]
|
:end-before: [Ex. 19]
|
||||||
|
|
||||||
|
|
@ -588,6 +625,7 @@ this custom Axis.
|
||||||
:class:`~geometry.Pos` then the part would be offset from the workplane by the vertex z-position.
|
:class:`~geometry.Pos` then the part would be offset from the workplane by the vertex z-position.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 19]
|
:start-after: [Ex. 19]
|
||||||
:end-before: [Ex. 19]
|
:end-before: [Ex. 19]
|
||||||
|
|
||||||
|
|
@ -606,12 +644,14 @@ negative x-direction. The resulting Plane is offset from the original position.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 20]
|
:start-after: [Ex. 20]
|
||||||
:end-before: [Ex. 20]
|
:end-before: [Ex. 20]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 20]
|
:start-after: [Ex. 20]
|
||||||
:end-before: [Ex. 20]
|
:end-before: [Ex. 20]
|
||||||
|
|
||||||
|
|
@ -630,12 +670,14 @@ positioning another cylinder perpendicular and halfway along the first.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 21]
|
:start-after: [Ex. 21]
|
||||||
:end-before: [Ex. 21]
|
:end-before: [Ex. 21]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 21]
|
:start-after: [Ex. 21]
|
||||||
:end-before: [Ex. 21]
|
:end-before: [Ex. 21]
|
||||||
|
|
||||||
|
|
@ -656,6 +698,7 @@ example.
|
||||||
Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane.
|
Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 22]
|
:start-after: [Ex. 22]
|
||||||
:end-before: [Ex. 22]
|
:end-before: [Ex. 22]
|
||||||
|
|
||||||
|
|
@ -664,6 +707,7 @@ example.
|
||||||
Use the operator ``*`` to relocate the plane (post-multiplication!).
|
Use the operator ``*`` to relocate the plane (post-multiplication!).
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 22]
|
:start-after: [Ex. 22]
|
||||||
:end-before: [Ex. 22]
|
:end-before: [Ex. 22]
|
||||||
|
|
||||||
|
|
@ -690,12 +734,14 @@ It is highly recommended to view your sketch before you attempt to call revolve.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 23]
|
:start-after: [Ex. 23]
|
||||||
:end-before: [Ex. 23]
|
:end-before: [Ex. 23]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 23]
|
:start-after: [Ex. 23]
|
||||||
:end-before: [Ex. 23]
|
:end-before: [Ex. 23]
|
||||||
|
|
||||||
|
|
@ -716,12 +762,14 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 24]
|
:start-after: [Ex. 24]
|
||||||
:end-before: [Ex. 24]
|
:end-before: [Ex. 24]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 24]
|
:start-after: [Ex. 24]
|
||||||
:end-before: [Ex. 24]
|
:end-before: [Ex. 24]
|
||||||
|
|
||||||
|
|
@ -739,6 +787,7 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
|
||||||
BuildSketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
|
BuildSketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 25]
|
:start-after: [Ex. 25]
|
||||||
:end-before: [Ex. 25]
|
:end-before: [Ex. 25]
|
||||||
|
|
||||||
|
|
@ -747,6 +796,7 @@ Loft can behave unexpectedly when the input faces are not parallel to each other
|
||||||
Sketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
|
Sketch faces can be transformed with a 2D :meth:`~operations_generic.offset`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 25]
|
:start-after: [Ex. 25]
|
||||||
:end-before: [Ex. 25]
|
:end-before: [Ex. 25]
|
||||||
|
|
||||||
|
|
@ -772,12 +822,14 @@ Note that self intersecting edges and/or faces can break both 2D and 3D offsets.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 26]
|
:start-after: [Ex. 26]
|
||||||
:end-before: [Ex. 26]
|
:end-before: [Ex. 26]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 26]
|
:start-after: [Ex. 26]
|
||||||
:end-before: [Ex. 26]
|
:end-before: [Ex. 26]
|
||||||
|
|
||||||
|
|
@ -796,12 +848,14 @@ a face and offset half the width of the box.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 27]
|
:start-after: [Ex. 27]
|
||||||
:end-before: [Ex. 27]
|
:end-before: [Ex. 27]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 27]
|
:start-after: [Ex. 27]
|
||||||
:end-before: [Ex. 27]
|
:end-before: [Ex. 27]
|
||||||
|
|
||||||
|
|
@ -820,6 +874,7 @@ a face and offset half the width of the box.
|
||||||
use the faces of this object to cut holes in a sphere.
|
use the faces of this object to cut holes in a sphere.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 28]
|
:start-after: [Ex. 28]
|
||||||
:end-before: [Ex. 28]
|
:end-before: [Ex. 28]
|
||||||
|
|
||||||
|
|
@ -828,6 +883,7 @@ a face and offset half the width of the box.
|
||||||
We create a triangular prism and then later use the faces of this object to cut holes in a sphere.
|
We create a triangular prism and then later use the faces of this object to cut holes in a sphere.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 28]
|
:start-after: [Ex. 28]
|
||||||
:end-before: [Ex. 28]
|
:end-before: [Ex. 28]
|
||||||
|
|
||||||
|
|
@ -849,12 +905,14 @@ the bottle opening.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 29]
|
:start-after: [Ex. 29]
|
||||||
:end-before: [Ex. 29]
|
:end-before: [Ex. 29]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 29]
|
:start-after: [Ex. 29]
|
||||||
:end-before: [Ex. 29]
|
:end-before: [Ex. 29]
|
||||||
|
|
||||||
|
|
@ -874,12 +932,14 @@ create a closed line that is made into a face and extruded.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 30]
|
:start-after: [Ex. 30]
|
||||||
:end-before: [Ex. 30]
|
:end-before: [Ex. 30]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 30]
|
:start-after: [Ex. 30]
|
||||||
:end-before: [Ex. 30]
|
:end-before: [Ex. 30]
|
||||||
|
|
||||||
|
|
@ -899,12 +959,14 @@ rotates any "children" groups by default.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 31]
|
:start-after: [Ex. 31]
|
||||||
:end-before: [Ex. 31]
|
:end-before: [Ex. 31]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 31]
|
:start-after: [Ex. 31]
|
||||||
:end-before: [Ex. 31]
|
:end-before: [Ex. 31]
|
||||||
|
|
||||||
|
|
@ -927,12 +989,14 @@ separate calls to :meth:`~operations_part.extrude`.
|
||||||
adding these faces until the for-loop.
|
adding these faces until the for-loop.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 32]
|
:start-after: [Ex. 32]
|
||||||
:end-before: [Ex. 32]
|
:end-before: [Ex. 32]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 32]
|
:start-after: [Ex. 32]
|
||||||
:end-before: [Ex. 32]
|
:end-before: [Ex. 32]
|
||||||
|
|
||||||
|
|
@ -954,6 +1018,7 @@ progressively modify the size of each square.
|
||||||
The function returns a :class:`~build_sketch.BuildSketch`.
|
The function returns a :class:`~build_sketch.BuildSketch`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 33]
|
:start-after: [Ex. 33]
|
||||||
:end-before: [Ex. 33]
|
:end-before: [Ex. 33]
|
||||||
|
|
||||||
|
|
@ -962,6 +1027,7 @@ progressively modify the size of each square.
|
||||||
The function returns a ``Sketch`` object.
|
The function returns a ``Sketch`` object.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 33]
|
:start-after: [Ex. 33]
|
||||||
:end-before: [Ex. 33]
|
:end-before: [Ex. 33]
|
||||||
|
|
||||||
|
|
@ -983,6 +1049,7 @@ progressively modify the size of each square.
|
||||||
the 2nd "World" text on the top of the "Hello" text.
|
the 2nd "World" text on the top of the "Hello" text.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 34]
|
:start-after: [Ex. 34]
|
||||||
:end-before: [Ex. 34]
|
:end-before: [Ex. 34]
|
||||||
|
|
||||||
|
|
@ -993,6 +1060,7 @@ progressively modify the size of each square.
|
||||||
the ``topf`` variable to select the same face and deboss (indented) the text "World".
|
the ``topf`` variable to select the same face and deboss (indented) the text "World".
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 34]
|
:start-after: [Ex. 34]
|
||||||
:end-before: [Ex. 34]
|
:end-before: [Ex. 34]
|
||||||
|
|
||||||
|
|
@ -1012,6 +1080,7 @@ progressively modify the size of each square.
|
||||||
arc for two instances of :class:`~objects_sketch.SlotArc`.
|
arc for two instances of :class:`~objects_sketch.SlotArc`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 35]
|
:start-after: [Ex. 35]
|
||||||
:end-before: [Ex. 35]
|
:end-before: [Ex. 35]
|
||||||
|
|
||||||
|
|
@ -1021,6 +1090,7 @@ progressively modify the size of each square.
|
||||||
a :class:`~objects_curve.RadiusArc` to create an arc for two instances of :class:`~operations_sketch.SlotArc`.
|
a :class:`~objects_curve.RadiusArc` to create an arc for two instances of :class:`~operations_sketch.SlotArc`.
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 35]
|
:start-after: [Ex. 35]
|
||||||
:end-before: [Ex. 35]
|
:end-before: [Ex. 35]
|
||||||
|
|
||||||
|
|
@ -1041,11 +1111,14 @@ with ``Until.NEXT`` or ``Until.LAST``.
|
||||||
* **Builder mode**
|
* **Builder mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples.py
|
.. literalinclude:: general_examples.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 36]
|
:start-after: [Ex. 36]
|
||||||
:end-before: [Ex. 36]
|
:end-before: [Ex. 36]
|
||||||
|
|
||||||
* **Algebra mode**
|
* **Algebra mode**
|
||||||
|
|
||||||
.. literalinclude:: general_examples_algebra.py
|
.. literalinclude:: general_examples_algebra.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Ex. 36]
|
:start-after: [Ex. 36]
|
||||||
:end-before: [Ex. 36]
|
:end-before: [Ex. 36]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,14 @@ A rigid joint positions two components relative to each another with no freedom
|
||||||
and a ``joint_location`` which defines both the position and orientation of the joint (see
|
and a ``joint_location`` which defines both the position and orientation of the joint (see
|
||||||
:class:`~geometry.Location`) - as follows:
|
:class:`~geometry.Location`) - as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1))
|
RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1))
|
||||||
|
|
||||||
Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to
|
Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to
|
||||||
repositioning another part relative to ``self`` which stay fixed - as follows:
|
repositioning another part relative to ``self`` which stay fixed - as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"])
|
pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"])
|
||||||
|
|
||||||
|
|
@ -70,6 +70,7 @@ flanges are attached to the ends of a curved pipe:
|
||||||
.. image:: assets/rigid_joints_pipe.png
|
.. image:: assets/rigid_joints_pipe.png
|
||||||
|
|
||||||
.. literalinclude:: rigid_joints_pipe.py
|
.. literalinclude:: rigid_joints_pipe.py
|
||||||
|
:language: build123d
|
||||||
:emphasize-lines: 19-20, 23-24
|
:emphasize-lines: 19-20, 23-24
|
||||||
|
|
||||||
Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method
|
Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method
|
||||||
|
|
@ -132,6 +133,7 @@ Component moves along a single axis as with a sliding latch shown here:
|
||||||
The code to generate these components follows:
|
The code to generate these components follows:
|
||||||
|
|
||||||
.. literalinclude:: slide_latch.py
|
.. literalinclude:: slide_latch.py
|
||||||
|
:language: build123d
|
||||||
:emphasize-lines: 30, 52, 55
|
:emphasize-lines: 30, 52, 55
|
||||||
|
|
||||||
.. image:: assets/joint-latch.png
|
.. image:: assets/joint-latch.png
|
||||||
|
|
@ -193,6 +195,7 @@ is found within a rod end as shown here:
|
||||||
.. image:: assets/rod_end.png
|
.. image:: assets/rod_end.png
|
||||||
|
|
||||||
.. literalinclude:: rod_end.py
|
.. literalinclude:: rod_end.py
|
||||||
|
:language: build123d
|
||||||
:emphasize-lines: 40-44,51,53
|
:emphasize-lines: 40-44,51,53
|
||||||
|
|
||||||
Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt
|
Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt
|
||||||
|
|
|
||||||
|
|
@ -12,26 +12,26 @@ Object arithmetic
|
||||||
|
|
||||||
- Creating a box and a cylinder centered at ``(0, 0, 0)``
|
- Creating a box and a cylinder centered at ``(0, 0, 0)``
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
b = Box(1, 2, 3)
|
b = Box(1, 2, 3)
|
||||||
c = Cylinder(0.2, 5)
|
c = Cylinder(0.2, 5)
|
||||||
|
|
||||||
- Fusing a box and a cylinder
|
- Fusing a box and a cylinder
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
r = Box(1, 2, 3) + Cylinder(0.2, 5)
|
r = Box(1, 2, 3) + Cylinder(0.2, 5)
|
||||||
|
|
||||||
- Cutting a cylinder from a box
|
- Cutting a cylinder from a box
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
r = Box(1, 2, 3) - Cylinder(0.2, 5)
|
r = Box(1, 2, 3) - Cylinder(0.2, 5)
|
||||||
|
|
||||||
- Intersecting a box and a cylinder
|
- Intersecting a box and a cylinder
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
r = Box(1, 2, 3) & Cylinder(0.2, 5)
|
r = Box(1, 2, 3) & Cylinder(0.2, 5)
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ The generic forms of object placement are:
|
||||||
|
|
||||||
1. Placement on ``plane`` or at ``location`` relative to XY plane:
|
1. Placement on ``plane`` or at ``location`` relative to XY plane:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
plane * alg_compound
|
plane * alg_compound
|
||||||
location * alg_compound
|
location * alg_compound
|
||||||
|
|
@ -62,7 +62,7 @@ The generic forms of object placement are:
|
||||||
2. Placement on the ``plane`` and then moved relative to the ``plane`` by ``location``
|
2. Placement on the ``plane`` and then moved relative to the ``plane`` by ``location``
|
||||||
(the location is relative to the local coordinate system of the plane).
|
(the location is relative to the local coordinate system of the plane).
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
plane * location * alg_compound
|
plane * location * alg_compound
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ Examples:
|
||||||
|
|
||||||
- Box on the ``XY`` plane, centered at `(0, 0, 0)` (both forms are equivalent):
|
- Box on the ``XY`` plane, centered at `(0, 0, 0)` (both forms are equivalent):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XY * Box(1, 2, 3)
|
Plane.XY * Box(1, 2, 3)
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ Examples:
|
||||||
|
|
||||||
- Box on the ``XY`` plane centered at `(0, 1, 0)` (all three are equivalent):
|
- Box on the ``XY`` plane centered at `(0, 1, 0)` (all three are equivalent):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XY * Pos(0, 1, 0) * Box(1, 2, 3)
|
Plane.XY * Pos(0, 1, 0) * Box(1, 2, 3)
|
||||||
|
|
||||||
|
|
@ -96,21 +96,21 @@ Examples:
|
||||||
|
|
||||||
- Box on plane ``Plane.XZ``:
|
- Box on plane ``Plane.XZ``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XZ * Box(1, 2, 3)
|
Plane.XZ * Box(1, 2, 3)
|
||||||
|
|
||||||
- Box on plane ``Plane.XZ`` with a location ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane, i.e.,
|
- Box on plane ``Plane.XZ`` with a location ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane, i.e.,
|
||||||
using the x-, y- and z-axis of the ``XZ`` plane:
|
using the x-, y- and z-axis of the ``XZ`` plane:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XZ * Pos(1, 2, 3) * Box(1, 2, 3)
|
Plane.XZ * Pos(1, 2, 3) * Box(1, 2, 3)
|
||||||
|
|
||||||
- Box on plane ``Plane.XZ`` moved to ``(X=1, Y=2, Z=3)`` relative to this plane and rotated there
|
- Box on plane ``Plane.XZ`` moved to ``(X=1, Y=2, Z=3)`` relative to this plane and rotated there
|
||||||
by the angles `(X=0, Y=100, Z=45)` around ``Plane.XZ`` axes:
|
by the angles `(X=0, Y=100, Z=45)` around ``Plane.XZ`` axes:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XZ * Pos(1, 2, 3) * Rot(0, 100, 45) * Box(1, 2, 3)
|
Plane.XZ * Pos(1, 2, 3) * Rot(0, 100, 45) * Box(1, 2, 3)
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ Examples:
|
||||||
- Box on plane ``Plane.XZ`` rotated on this plane by the angles ``(X=0, Y=100, Z=45)`` (using the
|
- Box on plane ``Plane.XZ`` rotated on this plane by the angles ``(X=0, Y=100, Z=45)`` (using the
|
||||||
x-, y- and z-axis of the ``XZ`` plane) and then moved to ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane:
|
x-, y- and z-axis of the ``XZ`` plane) and then moved to ``(X=1, Y=2, Z=3)`` relative to the ``XZ`` plane:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
Plane.XZ * Rot(0, 100, 45) * Pos(0,1,2) * Box(1, 2, 3)
|
Plane.XZ * Rot(0, 100, 45) * Pos(0,1,2) * Box(1, 2, 3)
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ Combing both concepts
|
||||||
|
|
||||||
**Object arithmetic** and **Placement at locations** can be combined:
|
**Object arithmetic** and **Placement at locations** can be combined:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
b = Plane.XZ * Rot(X=30) * Box(1, 2, 3) + Plane.YZ * Pos(X=-1) * Cylinder(0.2, 5)
|
b = Plane.XZ * Rot(X=30) * Box(1, 2, 3) + Plane.YZ * Pos(X=-1) * Cylinder(0.2, 5)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ Example Workflow
|
||||||
|
|
||||||
Here is an example of using a Builder to create a simple part:
|
Here is an example of using a Builder to create a simple part:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
from build123d import *
|
from build123d import *
|
||||||
|
|
||||||
|
|
@ -117,21 +117,21 @@ class for further processing.
|
||||||
One can access the objects created by these builders by referencing the appropriate
|
One can access the objects created by these builders by referencing the appropriate
|
||||||
instance variable. For example:
|
instance variable. For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as my_part:
|
with BuildPart() as my_part:
|
||||||
...
|
...
|
||||||
|
|
||||||
show_object(my_part.part)
|
show_object(my_part.part)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch() as my_sketch:
|
with BuildSketch() as my_sketch:
|
||||||
...
|
...
|
||||||
|
|
||||||
show_object(my_sketch.sketch)
|
show_object(my_sketch.sketch)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildLine() as my_line:
|
with BuildLine() as my_line:
|
||||||
...
|
...
|
||||||
|
|
@ -144,7 +144,7 @@ Implicit Builder Instance Variables
|
||||||
One might expect to have to reference a builder's instance variable when using
|
One might expect to have to reference a builder's instance variable when using
|
||||||
objects or operations that impact that builder like this:
|
objects or operations that impact that builder like this:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part_builder:
|
with BuildPart() as part_builder:
|
||||||
Box(part_builder, 10,10,10)
|
Box(part_builder, 10,10,10)
|
||||||
|
|
@ -153,7 +153,7 @@ Instead, build123d determines from the scope of the object or operation which
|
||||||
builder it applies to thus eliminating the need for the user to provide this
|
builder it applies to thus eliminating the need for the user to provide this
|
||||||
information - as follows:
|
information - as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part_builder:
|
with BuildPart() as part_builder:
|
||||||
Box(10,10,10)
|
Box(10,10,10)
|
||||||
|
|
@ -175,7 +175,7 @@ be generated on any plane which allows users to put a workplane where they are w
|
||||||
and then work in local 2D coordinate space.
|
and then work in local 2D coordinate space.
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart(Plane.XY) as example:
|
with BuildPart(Plane.XY) as example:
|
||||||
... # a 3D-part
|
... # a 3D-part
|
||||||
|
|
@ -199,7 +199,7 @@ One is not limited to a single workplane at a time. In the following example all
|
||||||
faces of the first box are used to define workplanes which are then used to position
|
faces of the first box are used to define workplanes which are then used to position
|
||||||
rotated boxes.
|
rotated boxes.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
import build123d as bd
|
import build123d as bd
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ When positioning objects or operations within a builder Location Contexts are us
|
||||||
function in a very similar was to the builders in that they create a context where one or
|
function in a very similar was to the builders in that they create a context where one or
|
||||||
more locations are active within a scope. For example:
|
more locations are active within a scope. For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart():
|
with BuildPart():
|
||||||
with Locations((0,10),(0,-10)):
|
with Locations((0,10),(0,-10)):
|
||||||
|
|
@ -244,7 +244,7 @@ its scope - much as the hour and minute indicator on an analogue clock.
|
||||||
Also note that the locations are local to the current location(s) - i.e. ``Locations`` can be
|
Also note that the locations are local to the current location(s) - i.e. ``Locations`` can be
|
||||||
nested. It's easy for a user to retrieve the global locations:
|
nested. It's easy for a user to retrieve the global locations:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with Locations(Plane.XY, Plane.XZ):
|
with Locations(Plane.XY, Plane.XZ):
|
||||||
locs = GridLocations(1, 1, 2, 2)
|
locs = GridLocations(1, 1, 2, 2)
|
||||||
|
|
@ -271,7 +271,7 @@ an iterable of objects is often required (often a ShapeList).
|
||||||
|
|
||||||
Here is the definition of :meth:`~operations_generic.fillet` to help illustrate:
|
Here is the definition of :meth:`~operations_generic.fillet` to help illustrate:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
def fillet(
|
def fillet(
|
||||||
objects: Union[Union[Edge, Vertex], Iterable[Union[Edge, Vertex]]],
|
objects: Union[Union[Edge, Vertex], Iterable[Union[Edge, Vertex]]],
|
||||||
|
|
@ -281,7 +281,7 @@ Here is the definition of :meth:`~operations_generic.fillet` to help illustrate:
|
||||||
To use this fillet operation, an edge or vertex or iterable of edges or
|
To use this fillet operation, an edge or vertex or iterable of edges or
|
||||||
vertices must be provided followed by a fillet radius with or without the keyword as follows:
|
vertices must be provided followed by a fillet radius with or without the keyword as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as pipes:
|
with BuildPart() as pipes:
|
||||||
Box(10, 10, 10, rotation=(10, 20, 30))
|
Box(10, 10, 10, rotation=(10, 20, 30))
|
||||||
|
|
@ -297,7 +297,7 @@ Combination Modes
|
||||||
Almost all objects or operations have a ``mode`` parameter which is defined by the
|
Almost all objects or operations have a ``mode`` parameter which is defined by the
|
||||||
``Mode`` Enum class as follows:
|
``Mode`` Enum class as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
class Mode(Enum):
|
class Mode(Enum):
|
||||||
ADD = auto()
|
ADD = auto()
|
||||||
|
|
@ -329,7 +329,7 @@ build123d stores points (to be specific ``Location`` (s)) internally to be used
|
||||||
positions for the placement of new objects. By default, a single location
|
positions for the placement of new objects. By default, a single location
|
||||||
will be created at the origin of the given workplane such that:
|
will be created at the origin of the given workplane such that:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as pipes:
|
with BuildPart() as pipes:
|
||||||
Box(10, 10, 10, rotation=(10, 20, 30))
|
Box(10, 10, 10, rotation=(10, 20, 30))
|
||||||
|
|
@ -338,7 +338,7 @@ will create a single 10x10x10 box centered at (0,0,0) - by default objects are
|
||||||
centered. One can create multiple objects by pushing points prior to creating
|
centered. One can create multiple objects by pushing points prior to creating
|
||||||
objects as follows:
|
objects as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as pipes:
|
with BuildPart() as pipes:
|
||||||
with Locations((-10, -10, -10), (10, 10, 10)):
|
with Locations((-10, -10, -10), (10, 10, 10)):
|
||||||
|
|
@ -370,7 +370,7 @@ Builder's Pending Objects
|
||||||
When a builder exits, it will push the object created back to its parent if
|
When a builder exits, it will push the object created back to its parent if
|
||||||
there was one. Here is an example:
|
there was one. Here is an example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
height, width, thickness, f_rad = 60, 80, 20, 10
|
height, width, thickness, f_rad = 60, 80, 20, 10
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
Location arithmetic for algebra mode
|
Location arithmetic for algebra mode
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
|
||||||
Position a shape relative to the XY plane
|
Position a shape relative to the XY plane
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
For the following use the helper function:
|
For the following use the helper function:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
def location_symbol(location: Location, scale: float = 1) -> Compound:
|
def location_symbol(location: Location, scale: float = 1) -> Compound:
|
||||||
return Compound.make_triad(axes_scale=scale).locate(location)
|
return Compound.make_triad(axes_scale=scale).locate(location)
|
||||||
|
|
@ -19,134 +18,131 @@ For the following use the helper function:
|
||||||
circle = Circle(scale * .8).edge()
|
circle = Circle(scale * .8).edge()
|
||||||
return (triad + circle).locate(plane.location)
|
return (triad + circle).locate(plane.location)
|
||||||
|
|
||||||
|
|
||||||
1. **Positioning at a location**
|
1. **Positioning at a location**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1, 2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
|
|
||||||
.. image:: assets/location-example-01.png
|
.. image:: assets/location-example-01.png
|
||||||
|
|
||||||
2) **Positioning on a plane**
|
2) **Positioning on a plane**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
plane = Plane.XZ
|
plane = Plane.XZ
|
||||||
|
|
||||||
face = plane * Rectangle(1, 2)
|
face = plane * Rectangle(1, 2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(plane_symbol(plane), name="plane")
|
show_object(plane_symbol(plane), name="plane")
|
||||||
|
|
||||||
.. image:: assets/location-example-07.png
|
.. image:: assets/location-example-07.png
|
||||||
|
|
||||||
Note that the ``x``-axis and the ``y``-axis of the plane are on the ``x``-axis and the ``z``-axis of the world coordinate system (red and blue axis)
|
|
||||||
|
|
||||||
|
Note: The ``x``-axis and the ``y``-axis of the plane are on the ``x``-axis and the ``z``-axis of the world coordinate system (red and blue axis).
|
||||||
|
|
||||||
Relative positioning to a plane
|
Relative positioning to a plane
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
1. **Position an object on a plane relative to the plane**
|
1. **Position an object on a plane relative to the plane**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1,2)
|
||||||
|
|
||||||
box = Plane(loc) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
box = Plane(loc) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
||||||
# box = Plane(face.location) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
# box = Plane(face.location) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
||||||
# box = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
# box = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
show_object(box, name="box")
|
show_object(box, name="box")
|
||||||
|
|
||||||
.. image:: assets/location-example-02.png
|
.. image:: assets/location-example-02.png
|
||||||
|
|
||||||
The ``x``, ``y``, ``z`` components of ``Pos(0.2, 0.4, 0.1)`` are relative to the ``x``-axis, ``y``-axis or
|
The ``X``, ``Y``, ``Z`` components of ``Pos(0.2, 0.4, 0.1)`` are relative to the ``x``-axis, ``y``-axis or
|
||||||
``z``-axis of the underlying location ``loc``.
|
``z``-axis of the underlying location ``loc``.
|
||||||
|
|
||||||
Note: ``Plane(loc) *``, ``Plane(face.location) *`` and ``loc *`` are equivalent in this example.
|
Note: ``Plane(loc) *``, ``Plane(face.location) *`` and ``loc *`` are equivalent in this example.
|
||||||
|
|
||||||
2. **Rotate an object on a plane relative to the plane**
|
2. **Rotate an object on a plane relative to the plane**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1,2)
|
||||||
|
|
||||||
box = Plane(loc) * Rot(z=80) * Box(0.2, 0.2, 0.2)
|
box = Plane(loc) * Rot(Z=80) * Box(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
show_object(box, name="box")
|
show_object(box, name="box")
|
||||||
|
|
||||||
.. image:: assets/location-example-03.png
|
.. image:: assets/location-example-03.png
|
||||||
|
|
||||||
The box is rotated via ``Rot(z=80)`` around the ``z``-axis of the underlying location
|
The box is rotated via ``Rot(Z=80)`` around the ``z``-axis of the underlying location
|
||||||
(and not of the z-axis of the world).
|
(and not of the z-axis of the world).
|
||||||
|
|
||||||
More general:
|
More general:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1,2)
|
||||||
|
|
||||||
box = loc * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
|
box = loc * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
show_object(box, name="box")
|
show_object(box, name="box")
|
||||||
|
|
||||||
.. image:: assets/location-example-04.png
|
.. image:: assets/location-example-04.png
|
||||||
|
|
||||||
The box is rotated via ``Rot(20, 40, 80)`` around all three axes relative to the plane.
|
The box is rotated via ``Rot(20, 40, 80)`` around all three axes relative to the plane.
|
||||||
|
|
||||||
3. **Rotate and position an object relative to a location**
|
3. **Rotate and position an object relative to a location**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1,2)
|
||||||
|
|
||||||
box = loc * Rot(20, 40, 80) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
box = loc * Rot(20, 40, 80) * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
show_object(box, name="box")
|
show_object(box, name="box")
|
||||||
show_object(location_symbol(loc * Rot(20, 40, 80), 0.5), options={"color":(0, 255, 255)}, name="local_location")
|
show_object(location_symbol(loc * Rot(20, 40, 80), 0.5), options={"color":(0, 255, 255)}, name="local_location")
|
||||||
|
|
||||||
.. image:: assets/location-example-05.png
|
.. image:: assets/location-example-05.png
|
||||||
|
|
||||||
The box is positioned via ``Pos(0.2, 0.4, 0.1)`` relative to the location ``loc * Rot(20, 40, 80)``
|
The box is positioned via ``Pos(0.2, 0.4, 0.1)`` relative to the location ``loc * Rot(20, 40, 80)``
|
||||||
|
|
||||||
4. **Position and rotate an object relative to a location**
|
4. **Position and rotate an object relative to a location**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
|
||||||
|
|
||||||
face = loc * Rectangle(1,2)
|
face = loc * Rectangle(1,2)
|
||||||
|
|
||||||
box = loc * Pos(0.2, 0.4, 0.1) * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
|
box = loc * Pos(0.2, 0.4, 0.1) * Rot(20, 40, 80) * Box(0.2, 0.2, 0.2)
|
||||||
|
|
||||||
show_object(face, name="face")
|
show_object(face, name="face")
|
||||||
show_object(location_symbol(loc), name="location")
|
show_object(location_symbol(loc), name="location")
|
||||||
show_object(box, name="box")
|
show_object(box, name="box")
|
||||||
show_object(location_symbol(loc * Pos(0.2, 0.4, 0.1), 0.5), options={"color":(0, 255, 255)}, name="local_location")
|
show_object(location_symbol(loc * Pos(0.2, 0.4, 0.1), 0.5), options={"color":(0, 255, 255)}, name="local_location")
|
||||||
|
|
||||||
.. image:: assets/location-example-06.png
|
.. image:: assets/location-example-06.png
|
||||||
|
|
||||||
Note: This is the same as `box = loc * Location((0.2, 0.4, 0.1), (20, 40, 80)) * Box(0.2, 0.2, 0.2)`
|
|
||||||
|
|
||||||
|
Note: This is the same as ``box = loc * Location((0.2, 0.4, 0.1), (20, 40, 80)) * Box(0.2, 0.2, 0.2)``
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ construction process. The following tools are commonly used to specify locations
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with Locations((10, 20, 30)):
|
with Locations((10, 20, 30)):
|
||||||
Box(5, 5, 5)
|
Box(5, 5, 5)
|
||||||
|
|
@ -42,7 +42,7 @@ an existing one.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
rotated_box = Rotation(45, 0, 0) * box
|
rotated_box = Rotation(45, 0, 0) * box
|
||||||
|
|
||||||
|
|
@ -55,13 +55,13 @@ Position
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
- **Absolute Position:** Set the position directly.
|
- **Absolute Position:** Set the position directly.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.position = (x, y, z)
|
shape.position = (x, y, z)
|
||||||
|
|
||||||
- **Relative Position:** Adjust the position incrementally.
|
- **Relative Position:** Adjust the position incrementally.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.position += (x, y, z)
|
shape.position += (x, y, z)
|
||||||
shape.position -= (x, y, z)
|
shape.position -= (x, y, z)
|
||||||
|
|
@ -71,13 +71,13 @@ Orientation
|
||||||
^^^^^^^^^^^
|
^^^^^^^^^^^
|
||||||
- **Absolute Orientation:** Set the orientation directly.
|
- **Absolute Orientation:** Set the orientation directly.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.orientation = (X, Y, Z)
|
shape.orientation = (X, Y, Z)
|
||||||
|
|
||||||
- **Relative Orientation:** Adjust the orientation incrementally.
|
- **Relative Orientation:** Adjust the orientation incrementally.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.orientation += (X, Y, Z)
|
shape.orientation += (X, Y, Z)
|
||||||
shape.orientation -= (X, Y, Z)
|
shape.orientation -= (X, Y, Z)
|
||||||
|
|
@ -86,25 +86,25 @@ Movement Methods
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
- **Relative Move:**
|
- **Relative Move:**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.move(Location)
|
shape.move(Location)
|
||||||
|
|
||||||
- **Relative Move of Copy:**
|
- **Relative Move of Copy:**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
relocated_shape = shape.moved(Location)
|
relocated_shape = shape.moved(Location)
|
||||||
|
|
||||||
- **Absolute Move:**
|
- **Absolute Move:**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
shape.locate(Location)
|
shape.locate(Location)
|
||||||
|
|
||||||
- **Absolute Move of Copy:**
|
- **Absolute Move of Copy:**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
relocated_shape = shape.located(Location)
|
relocated_shape = shape.located(Location)
|
||||||
|
|
||||||
|
|
@ -119,12 +119,12 @@ Transformation a.k.a. Translation and Rotation
|
||||||
|
|
||||||
- **Translation:** Move a shape relative to its current position.
|
- **Translation:** Move a shape relative to its current position.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
relocated_shape = shape.translate(x, y, z)
|
relocated_shape = shape.translate(x, y, z)
|
||||||
|
|
||||||
- **Rotation:** Rotate a shape around a specified axis by a given angle.
|
- **Rotation:** Rotate a shape around a specified axis by a given angle.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
rotated_shape = shape.rotate(Axis, angle_in_degrees)
|
rotated_shape = shape.rotate(Axis, angle_in_degrees)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ For example, a :class:`~objects_part.Torus` is defined by a major and minor radi
|
||||||
Builder mode, objects are positioned with ``Locations`` while in Algebra mode, objects
|
Builder mode, objects are positioned with ``Locations`` while in Algebra mode, objects
|
||||||
are positioned with the ``*`` operator and shown in these examples:
|
are positioned with the ``*`` operator and shown in these examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as disk:
|
with BuildPart() as disk:
|
||||||
with BuildSketch():
|
with BuildSketch():
|
||||||
|
|
@ -18,7 +18,7 @@ are positioned with the ``*`` operator and shown in these examples:
|
||||||
Circle(d, mode=Mode.SUBTRACT)
|
Circle(d, mode=Mode.SUBTRACT)
|
||||||
extrude(amount=c)
|
extrude(amount=c)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
sketch = Circle(a) - Pos(b, 0.0) * Rectangle(c, c) - Pos(0.0, b) * Circle(d)
|
sketch = Circle(a) - Pos(b, 0.0) * Rectangle(c, c) - Pos(0.0, b) * Circle(d)
|
||||||
disk = extrude(sketch, c)
|
disk = extrude(sketch, c)
|
||||||
|
|
@ -36,7 +36,7 @@ right or left of each Axis. The following diagram shows how this alignment works
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch():
|
with BuildSketch():
|
||||||
Circle(1, align=(Align.MIN, Align.MIN))
|
Circle(1, align=(Align.MIN, Align.MIN))
|
||||||
|
|
@ -49,7 +49,7 @@ In 3D the ``align`` parameter also contains a Z align value but otherwise works
|
||||||
Note that the ``align`` will also accept a single ``Align`` value which will be used on all axes -
|
Note that the ``align`` will also accept a single ``Align`` value which will be used on all axes -
|
||||||
as shown here:
|
as shown here:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch():
|
with BuildSketch():
|
||||||
Circle(1, align=Align.MIN)
|
Circle(1, align=Align.MIN)
|
||||||
|
|
@ -519,6 +519,7 @@ Here is an example of a custom sketch object specially created as part of the de
|
||||||
this playing card storage box (:download:`see the playing_cards.py example <../examples/playing_cards.py>`):
|
this playing card storage box (:download:`see the playing_cards.py example <../examples/playing_cards.py>`):
|
||||||
|
|
||||||
.. literalinclude:: ../examples/playing_cards.py
|
.. literalinclude:: ../examples/playing_cards.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Club]
|
:start-after: [Club]
|
||||||
:end-before: [Club]
|
:end-before: [Club]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ Operations are functions that take objects as inputs and transform them into new
|
||||||
|
|
||||||
Here are a couple ways to use :func:`~operations_part.extrude`, in Builder and Algebra mode:
|
Here are a couple ways to use :func:`~operations_part.extrude`, in Builder and Algebra mode:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as cylinder:
|
with BuildPart() as cylinder:
|
||||||
with BuildSketch():
|
with BuildSketch():
|
||||||
Circle(radius)
|
Circle(radius)
|
||||||
extrude(amount=height)
|
extrude(amount=height)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
cylinder = extrude(Circle(radius), amount=height)
|
cylinder = extrude(Circle(radius), amount=height)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ It is important to note that standard list methods such as `sorted` or `filtered
|
||||||
be used to easily build complex selectors beyond what is available with the predefined
|
be used to easily build complex selectors beyond what is available with the predefined
|
||||||
sorts and filters. Here is an example of a custom filters:
|
sorts and filters. Here is an example of a custom filters:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch() as din:
|
with BuildSketch() as din:
|
||||||
...
|
...
|
||||||
|
|
@ -88,7 +88,7 @@ The :meth:`~topology.ShapeList.filter_by` method can take lambda expressions as
|
||||||
fluent chain of operations which enables integration of custom filters into a larger change of
|
fluent chain of operations which enables integration of custom filters into a larger change of
|
||||||
selectors as shown in this example:
|
selectors as shown in this example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
obj = Box(1, 1, 1) - Cylinder(0.2, 1)
|
obj = Box(1, 1, 1) - Cylinder(0.2, 1)
|
||||||
faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires())
|
faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires())
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ Code
|
||||||
----
|
----
|
||||||
|
|
||||||
.. literalinclude:: technical_drawing.py
|
.. literalinclude:: technical_drawing.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:start-after: [code]
|
:start-after: [code]
|
||||||
:end-before: [end]
|
:end-before: [end]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ consider a plate with four chamfered holes like this:
|
||||||
When selecting edges to be chamfered one might first select the face that these edges
|
When selecting edges to be chamfered one might first select the face that these edges
|
||||||
belong to then select the edges as shown here:
|
belong to then select the edges as shown here:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
from build123d import *
|
from build123d import *
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ a common OpenCascade Python wrapper (`OCP <https://github.com/CadQuery/OCP>`_) i
|
||||||
interchange objects both from CadQuery to build123d and vice-versa by transferring the ``wrapped``
|
interchange objects both from CadQuery to build123d and vice-versa by transferring the ``wrapped``
|
||||||
objects as follows (first from CadQuery to build123d):
|
objects as follows (first from CadQuery to build123d):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
import build123d as b3d
|
import build123d as b3d
|
||||||
b3d_solid = b3d.Solid.make_box(1,1,1)
|
b3d_solid = b3d.Solid.make_box(1,1,1)
|
||||||
|
|
@ -129,7 +129,7 @@ objects as follows (first from CadQuery to build123d):
|
||||||
|
|
||||||
Secondly, from build123d to CadQuery as follows:
|
Secondly, from build123d to CadQuery as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
import build123d as b3d
|
import build123d as b3d
|
||||||
import cadquery as cq
|
import cadquery as cq
|
||||||
|
|
@ -209,7 +209,7 @@ Why doesn't BuildSketch(Plane.XZ) work?
|
||||||
When creating a sketch not on the default ``Plane.XY`` users may expect that they are drawing directly
|
When creating a sketch not on the default ``Plane.XY`` users may expect that they are drawing directly
|
||||||
on the workplane / coordinate system provided. For example:
|
on the workplane / coordinate system provided. For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch(Plane.XZ) as vertical_sketch:
|
with BuildSketch(Plane.XZ) as vertical_sketch:
|
||||||
Rectangle(1, 1)
|
Rectangle(1, 1)
|
||||||
|
|
@ -229,7 +229,7 @@ Why does ``BuildSketch`` work this way? Consider an example where the user wants
|
||||||
plane not aligned with any Axis, as follows (this is often done when creating a sketch on a ``Face``
|
plane not aligned with any Axis, as follows (this is often done when creating a sketch on a ``Face``
|
||||||
of a 3D part but is simulated here by rotating a ``Plane``):
|
of a 3D part but is simulated here by rotating a ``Plane``):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch(Plane.YZ.rotated((123, 45, 6))) as custom_plane:
|
with BuildSketch(Plane.YZ.rotated((123, 45, 6))) as custom_plane:
|
||||||
Rectangle(1, 1, align=Align.MIN)
|
Rectangle(1, 1, align=Align.MIN)
|
||||||
|
|
@ -251,7 +251,7 @@ Why is BuildLine not working as expected within the scope of BuildSketch?
|
||||||
As described above, all sketching is done on a local ``Plane.XY``; however, the following
|
As described above, all sketching is done on a local ``Plane.XY``; however, the following
|
||||||
is a common issue:
|
is a common issue:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch() as sketch:
|
with BuildSketch() as sketch:
|
||||||
with BuildLine(Plane.XZ):
|
with BuildLine(Plane.XZ):
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ Overview
|
||||||
Both shape objects and builder objects have access to selector methods to select all of
|
Both shape objects and builder objects have access to selector methods to select all of
|
||||||
a feature as long as they can contain the feature being selected.
|
a feature as long as they can contain the feature being selected.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# In context
|
# In context
|
||||||
with BuildSketch() as context:
|
with BuildSketch() as context:
|
||||||
|
|
@ -70,7 +70,7 @@ existed in the referenced object before the last operation, nor the modifying ob
|
||||||
|
|
||||||
:class:`~build_enums.Select` as selector criteria is only valid for builder objects!
|
:class:`~build_enums.Select` as selector criteria is only valid for builder objects!
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
# In context
|
# In context
|
||||||
with BuildPart() as context:
|
with BuildPart() as context:
|
||||||
|
|
@ -85,7 +85,7 @@ existed in the referenced object before the last operation, nor the modifying ob
|
||||||
Create a simple part to demonstrate selectors. Select using the default criteria
|
Create a simple part to demonstrate selectors. Select using the default criteria
|
||||||
``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required.
|
``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(5, 5, 1)
|
Box(5, 5, 1)
|
||||||
|
|
@ -107,7 +107,7 @@ Create a simple part to demonstrate selectors. Select using the default criteria
|
||||||
|
|
||||||
Select features changed in the last operation with criteria ``Select.LAST``.
|
Select features changed in the last operation with criteria ``Select.LAST``.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(5, 5, 1)
|
Box(5, 5, 1)
|
||||||
|
|
@ -125,7 +125,7 @@ Select features changed in the last operation with criteria ``Select.LAST``.
|
||||||
Select only new edges from the last operation with ``Select.NEW``. This option is only
|
Select only new edges from the last operation with ``Select.NEW``. This option is only
|
||||||
available for a ``ShapeList`` of edges!
|
available for a ``ShapeList`` of edges!
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(5, 5, 1)
|
Box(5, 5, 1)
|
||||||
|
|
@ -142,7 +142,7 @@ This only returns new edges which are not reused from Box or Cylinder, in this c
|
||||||
the objects `intersect`. But what happens if the objects don't intersect and all the
|
the objects `intersect`. But what happens if the objects don't intersect and all the
|
||||||
edges are reused?
|
edges are reused?
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX))
|
Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX))
|
||||||
|
|
@ -164,7 +164,7 @@ only completely new edges created by the operation.
|
||||||
Chamfer and fillet modify the current object, but do not have new edges via
|
Chamfer and fillet modify the current object, but do not have new edges via
|
||||||
``Select.NEW``.
|
``Select.NEW``.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(5, 5, 1)
|
Box(5, 5, 1)
|
||||||
|
|
@ -187,7 +187,7 @@ another "combined" shape object and returns the edges new to the combined shape.
|
||||||
``new_edges`` is available both Algebra mode or Builder mode, but is necessary in
|
``new_edges`` is available both Algebra mode or Builder mode, but is necessary in
|
||||||
Algebra Mode where ``Select.NEW`` is unavailable
|
Algebra Mode where ``Select.NEW`` is unavailable
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
box = Box(5, 5, 1)
|
box = Box(5, 5, 1)
|
||||||
circle = Cylinder(2, 5)
|
circle = Cylinder(2, 5)
|
||||||
|
|
@ -200,7 +200,7 @@ Algebra Mode where ``Select.NEW`` is unavailable
|
||||||
``new_edges`` can also find edges created during a chamfer or fillet operation by
|
``new_edges`` can also find edges created during a chamfer or fillet operation by
|
||||||
comparing the object before the operation to the "combined" object.
|
comparing the object before the operation to the "combined" object.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
box = Box(5, 5, 1)
|
box = Box(5, 5, 1)
|
||||||
circle = Cylinder(2, 5)
|
circle = Cylinder(2, 5)
|
||||||
|
|
@ -263,7 +263,7 @@ Finally, the vertices can be captured with a list slice for the last 4 list item
|
||||||
items are sorted from least to greatest ``X`` position. Remember, ``ShapeList`` is a
|
items are sorted from least to greatest ``X`` position. Remember, ``ShapeList`` is a
|
||||||
subclass of ``list``, so any list slice can be used.
|
subclass of ``list``, so any list slice can be used.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
part.vertices().sort_by(Axis.X)[-4:]
|
part.vertices().sort_by(Axis.X)[-4:]
|
||||||
|
|
||||||
|
|
@ -320,7 +320,7 @@ group by ``SortBy.AREA``. The ``ShapeList`` of smallest faces is available from
|
||||||
list index. Finally, a ``ShapeList`` has access to selectors, so calling |edges| will
|
list index. Finally, a ``ShapeList`` has access to selectors, so calling |edges| will
|
||||||
return a new list of all edges in the previous list.
|
return a new list of all edges in the previous list.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
part.faces().group_by(SortBy.AREA)[0].edges())
|
part.faces().group_by(SortBy.AREA)[0].edges())
|
||||||
|
|
||||||
|
|
@ -368,7 +368,7 @@ might be with a list comprehension, however |filter_by| has the capability to ta
|
||||||
lambda function as a filter condition on the entire list. In this case, the normal of
|
lambda function as a filter condition on the entire list. In this case, the normal of
|
||||||
each face can be checked against a vector direction and filtered accordingly.
|
each face can be checked against a vector direction and filtered accordingly.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))
|
part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_geomtype.py
|
.. literalinclude:: examples/filter_geomtype.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 3, 8-13
|
:lines: 3, 8-13
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_geomtype.py
|
.. literalinclude:: examples/filter_geomtype.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 15
|
:lines: 15
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_geomtype_line.png
|
.. figure:: ../assets/topology_selection/filter_geomtype_line.png
|
||||||
|
|
@ -31,7 +31,7 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
|
||||||
|
|
|
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_geomtype.py
|
.. literalinclude:: examples/filter_geomtype.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 17
|
:lines: 17
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_geomtype_cylinder.png
|
.. figure:: ../assets/topology_selection/filter_geomtype_cylinder.png
|
||||||
|
|
@ -52,11 +52,11 @@ circular edges selects the counterbore faces that meet the joint criteria.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_all_edges_circle.py
|
.. literalinclude:: examples/filter_all_edges_circle.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 3, 8-41
|
:lines: 3, 8-41
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_all_edges_circle.py
|
.. literalinclude:: examples/filter_all_edges_circle.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 43-47
|
:lines: 43-47
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_all_edges_circle.png
|
.. figure:: ../assets/topology_selection/filter_all_edges_circle.png
|
||||||
|
|
@ -74,14 +74,14 @@ Plane will select faces parallel to the plane.
|
||||||
|
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
from build123d import *
|
from build123d import *
|
||||||
|
|
||||||
with BuildPart() as part:
|
with BuildPart() as part:
|
||||||
Box(1, 1, 1)
|
Box(1, 1, 1)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
part.faces().filter_by(Axis.Z)
|
part.faces().filter_by(Axis.Z)
|
||||||
part.faces().filter_by(Plane.XY)
|
part.faces().filter_by(Plane.XY)
|
||||||
|
|
@ -96,7 +96,7 @@ accomplish this with feature properties or methods. Here, we are looking for fac
|
||||||
the dot product of face normal and either the axis direction or the plane normal is about
|
the dot product of face normal and either the axis direction or the plane normal is about
|
||||||
to 0. The result is faces parallel to the axis or perpendicular to the plane.
|
to 0. The result is faces parallel to the axis or perpendicular to the plane.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Axis.Z.direction) < 1e-6)
|
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Axis.Z.direction) < 1e-6)
|
||||||
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Plane.XY.z_dir)) < 1e-6)
|
part.faces().filter_by(lambda f: abs(f.normal_at().dot(Plane.XY.z_dir)) < 1e-6)
|
||||||
|
|
@ -122,11 +122,11 @@ and then filtering for the specific inner wire by radius.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-16
|
:lines: 4, 9-16
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 18-21
|
:lines: 18-21
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_inner_wire_count.png
|
.. figure:: ../assets/topology_selection/filter_inner_wire_count.png
|
||||||
|
|
@ -140,7 +140,7 @@ axis and range. To do that we can filter for faces with 6 inner wires, sort for
|
||||||
select the top face, and then filter for the circular edges of the inner wires.
|
select the top face, and then filter for the circular edges of the inner wires.
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_inner_wire_count.py
|
.. literalinclude:: examples/filter_inner_wire_count.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 25-32
|
:lines: 25-32
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png
|
.. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png
|
||||||
|
|
@ -163,11 +163,11 @@ any line edges.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_nested.py
|
.. literalinclude:: examples/filter_nested.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-22
|
:lines: 4, 9-22
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_nested.py
|
.. literalinclude:: examples/filter_nested.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 26-32
|
:lines: 26-32
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_nested.png
|
.. figure:: ../assets/topology_selection/filter_nested.png
|
||||||
|
|
@ -186,7 +186,7 @@ different fillets accordingly. Then the ``Face`` ``is_circular_*`` properties ar
|
||||||
to highlight the resulting fillets.
|
to highlight the resulting fillets.
|
||||||
|
|
||||||
.. literalinclude:: examples/filter_shape_properties.py
|
.. literalinclude:: examples/filter_shape_properties.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 3-4, 8-22
|
:lines: 3-4, 8-22
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/filter_shape_properties.png
|
.. figure:: ../assets/topology_selection/filter_shape_properties.png
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ result knowing how many edges to expect.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/group_axis.py
|
.. literalinclude:: examples/group_axis.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-17
|
:lines: 4, 9-17
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/group_axis_without.png
|
.. figure:: ../assets/topology_selection/group_axis_without.png
|
||||||
|
|
@ -26,7 +26,7 @@ However, ``group_by`` can be used to first group all the edges by z-axis positio
|
||||||
group again by length. In both cases, you can select the desired edges from the last group.
|
group again by length. In both cases, you can select the desired edges from the last group.
|
||||||
|
|
||||||
.. literalinclude:: examples/group_axis.py
|
.. literalinclude:: examples/group_axis.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 21-22
|
:lines: 21-22
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/group_axis_with.png
|
.. figure:: ../assets/topology_selection/group_axis_with.png
|
||||||
|
|
@ -46,11 +46,11 @@ with the largest hole.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/group_hole_area.py
|
.. literalinclude:: examples/group_hole_area.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-17
|
:lines: 4, 9-17
|
||||||
|
|
||||||
.. literalinclude:: examples/group_hole_area.py
|
.. literalinclude:: examples/group_hole_area.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 21-24
|
:lines: 21-24
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/group_hole_area.png
|
.. figure:: ../assets/topology_selection/group_hole_area.png
|
||||||
|
|
@ -72,11 +72,11 @@ then the desired groups are selected with the ``group`` method using the lengths
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/group_properties_with_keys.py
|
.. literalinclude:: examples/group_properties_with_keys.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-26
|
:lines: 4, 9-26
|
||||||
|
|
||||||
.. literalinclude:: examples/group_properties_with_keys.py
|
.. literalinclude:: examples/group_properties_with_keys.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 30, 31
|
:lines: 30, 31
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/group_length_key.png
|
.. figure:: ../assets/topology_selection/group_length_key.png
|
||||||
|
|
@ -94,11 +94,11 @@ and then further specify only the edges the bearings and pins are installed from
|
||||||
.. dropdown:: Adding holes
|
.. dropdown:: Adding holes
|
||||||
|
|
||||||
.. literalinclude:: examples/group_properties_with_keys.py
|
.. literalinclude:: examples/group_properties_with_keys.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 35-43
|
:lines: 35-43
|
||||||
|
|
||||||
.. literalinclude:: examples/group_properties_with_keys.py
|
.. literalinclude:: examples/group_properties_with_keys.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 47-50
|
:lines: 47-50
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/group_radius_key.png
|
.. figure:: ../assets/topology_selection/group_radius_key.png
|
||||||
|
|
@ -109,7 +109,7 @@ and then further specify only the edges the bearings and pins are installed from
|
||||||
Note that ``group_by`` is not the only way to capture edges with a known property
|
Note that ``group_by`` is not the only way to capture edges with a known property
|
||||||
value! ``filter_by`` with a lambda expression can be used as well:
|
value! ``filter_by`` with a lambda expression can be used as well:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
radius_groups = part.edges().filter_by(GeomType.CIRCLE)
|
radius_groups = part.edges().filter_by(GeomType.CIRCLE)
|
||||||
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)
|
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ be used with``group_by``.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_sortby.py
|
.. literalinclude:: examples/sort_sortby.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 3, 8-13
|
:lines: 3, 8-13
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_sortby.py
|
.. literalinclude:: examples/sort_sortby.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 19-22
|
:lines: 19-22
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_sortby_length.png
|
.. figure:: ../assets/topology_selection/sort_sortby_length.png
|
||||||
|
|
@ -36,7 +36,7 @@ be used with``group_by``.
|
||||||
|
|
|
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_sortby.py
|
.. literalinclude:: examples/sort_sortby.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 24-27
|
:lines: 24-27
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_sortby_distance.png
|
.. figure:: ../assets/topology_selection/sort_sortby_distance.png
|
||||||
|
|
@ -57,11 +57,11 @@ the order is random.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_along_wire.py
|
.. literalinclude:: examples/sort_along_wire.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 3, 8-12
|
:lines: 3, 8-12
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_along_wire.py
|
.. literalinclude:: examples/sort_along_wire.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 14-15
|
:lines: 14-15
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_not_along_wire.png
|
.. figure:: ../assets/topology_selection/sort_not_along_wire.png
|
||||||
|
|
@ -73,7 +73,7 @@ Vertices may be sorted along the wire they fall on to create order. Notice the f
|
||||||
radii now increase in order.
|
radii now increase in order.
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_along_wire.py
|
.. literalinclude:: examples/sort_along_wire.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 26-28
|
:lines: 26-28
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_along_wire.png
|
.. figure:: ../assets/topology_selection/sort_along_wire.png
|
||||||
|
|
@ -94,11 +94,11 @@ edge can be found sorting along y-axis.
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_axis.py
|
.. literalinclude:: examples/sort_axis.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 4, 9-18
|
:lines: 4, 9-18
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_axis.py
|
.. literalinclude:: examples/sort_axis.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 22-24
|
:lines: 22-24
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_axis.png
|
.. figure:: ../assets/topology_selection/sort_axis.png
|
||||||
|
|
@ -118,11 +118,11 @@ Here we are sorting the boxes by distance from the origin, using an empty ``Vert
|
||||||
.. dropdown:: Setup
|
.. dropdown:: Setup
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_distance_from.py
|
.. literalinclude:: examples/sort_distance_from.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 2-5, 9-13
|
:lines: 2-5, 9-13
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_distance_from.py
|
.. literalinclude:: examples/sort_distance_from.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 15-16
|
:lines: 15-16
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_distance_from_origin.png
|
.. figure:: ../assets/topology_selection/sort_distance_from_origin.png
|
||||||
|
|
@ -135,7 +135,7 @@ property ``volume``, and getting the last (largest) box. Then, the boxes sorted
|
||||||
their distance from the largest box.
|
their distance from the largest box.
|
||||||
|
|
||||||
.. literalinclude:: examples/sort_distance_from.py
|
.. literalinclude:: examples/sort_distance_from.py
|
||||||
:language: python
|
:language: build123d
|
||||||
:lines: 19-20
|
:lines: 19-20
|
||||||
|
|
||||||
.. figure:: ../assets/topology_selection/sort_distance_from_largest.png
|
.. figure:: ../assets/topology_selection/sort_distance_from_largest.png
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ Party Pack 01-01 Bearing Bracket
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0101.py
|
.. literalinclude:: assets/ttt/ttt-ppp0101.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
|
|
||||||
.. _ttt-ppp0102:
|
.. _ttt-ppp0102:
|
||||||
|
|
@ -114,6 +115,7 @@ Party Pack 01-02 Post Cap
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0102.py
|
.. literalinclude:: assets/ttt/ttt-ppp0102.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0103:
|
.. _ttt-ppp0103:
|
||||||
|
|
||||||
|
|
@ -129,6 +131,7 @@ Party Pack 01-03 C Clamp Base
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0103.py
|
.. literalinclude:: assets/ttt/ttt-ppp0103.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0104:
|
.. _ttt-ppp0104:
|
||||||
|
|
||||||
|
|
@ -144,6 +147,7 @@ Party Pack 01-04 Angle Bracket
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0104.py
|
.. literalinclude:: assets/ttt/ttt-ppp0104.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0105:
|
.. _ttt-ppp0105:
|
||||||
|
|
||||||
|
|
@ -159,6 +163,7 @@ Party Pack 01-05 Paste Sleeve
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0105.py
|
.. literalinclude:: assets/ttt/ttt-ppp0105.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0106:
|
.. _ttt-ppp0106:
|
||||||
|
|
||||||
|
|
@ -174,6 +179,7 @@ Party Pack 01-06 Bearing Jig
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0106.py
|
.. literalinclude:: assets/ttt/ttt-ppp0106.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0107:
|
.. _ttt-ppp0107:
|
||||||
|
|
||||||
|
|
@ -189,6 +195,7 @@ Party Pack 01-07 Flanged Hub
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0107.py
|
.. literalinclude:: assets/ttt/ttt-ppp0107.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0108:
|
.. _ttt-ppp0108:
|
||||||
|
|
||||||
|
|
@ -204,6 +211,7 @@ Party Pack 01-08 Tie Plate
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0108.py
|
.. literalinclude:: assets/ttt/ttt-ppp0108.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0109:
|
.. _ttt-ppp0109:
|
||||||
|
|
||||||
|
|
@ -219,6 +227,7 @@ Party Pack 01-09 Corner Tie
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0109.py
|
.. literalinclude:: assets/ttt/ttt-ppp0109.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-ppp0110:
|
.. _ttt-ppp0110:
|
||||||
|
|
||||||
|
|
@ -234,6 +243,7 @@ Party Pack 01-10 Light Cap
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-ppp0110.py
|
.. literalinclude:: assets/ttt/ttt-ppp0110.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-23-02-02-sm_hanger:
|
.. _ttt-23-02-02-sm_hanger:
|
||||||
|
|
||||||
|
|
@ -249,6 +259,7 @@ Party Pack 01-10 Light Cap
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py
|
.. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-23-t-24:
|
.. _ttt-23-t-24:
|
||||||
|
|
||||||
|
|
@ -265,6 +276,7 @@ Party Pack 01-10 Light Cap
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py
|
.. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py
|
||||||
|
:language: build123d
|
||||||
|
|
||||||
.. _ttt-24-spo-06:
|
.. _ttt-24-spo-06:
|
||||||
|
|
||||||
|
|
@ -281,3 +293,4 @@ Party Pack 01-10 Light Cap
|
||||||
.. dropdown:: Reference Implementation
|
.. dropdown:: Reference Implementation
|
||||||
|
|
||||||
.. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py
|
.. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py
|
||||||
|
:language: build123d
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ Mirror parts of profiles across any axes of symmetry identified earlier.
|
||||||
|
|
||||||
*The build123d code to generate this profile is as follows:*
|
*The build123d code to generate this profile is as follows:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch() as sketch:
|
with BuildSketch() as sketch:
|
||||||
with BuildLine() as profile:
|
with BuildLine() as profile:
|
||||||
|
|
@ -109,7 +109,7 @@ Use the resulting geometry as sub-parts if needed.
|
||||||
*The next step in implementing our design in build123d is to convert the above sketch into
|
*The next step in implementing our design in build123d is to convert the above sketch into
|
||||||
a part by extruding it as shown in this code:*
|
a part by extruding it as shown in this code:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildPart() as bracket:
|
with BuildPart() as bracket:
|
||||||
with BuildSketch() as sketch:
|
with BuildSketch() as sketch:
|
||||||
|
|
@ -156,7 +156,7 @@ ensure the correct edges have been modified.
|
||||||
define these corners need to be isolated. The following code, placed to follow the previous
|
define these corners need to be isolated. The following code, placed to follow the previous
|
||||||
code block, captures just these edges:*
|
code block, captures just these edges:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1]
|
corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1]
|
||||||
fillet(corners, fillet_radius)
|
fillet(corners, fillet_radius)
|
||||||
|
|
@ -191,7 +191,7 @@ and functionality in the final assembly.
|
||||||
*Our example has two circular holes and a slot that need to be created. First we'll create
|
*Our example has two circular holes and a slot that need to be created. First we'll create
|
||||||
the two circular holes:*
|
the two circular holes:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with Locations(bracket.faces().sort_by(Axis.X)[-1]):
|
with Locations(bracket.faces().sort_by(Axis.X)[-1]):
|
||||||
Hole(hole_diameter / 2)
|
Hole(hole_diameter / 2)
|
||||||
|
|
@ -219,7 +219,7 @@ the two circular holes:*
|
||||||
*Next the slot needs to be created in the bracket with will be done by sketching a slot on
|
*Next the slot needs to be created in the bracket with will be done by sketching a slot on
|
||||||
the front of the bracket and extruding the sketch through the part.*
|
the front of the bracket and extruding the sketch through the part.*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
with BuildSketch(bracket.faces().sort_by(Axis.Y)[0]):
|
with BuildSketch(bracket.faces().sort_by(Axis.Y)[0]):
|
||||||
SlotOverall(20 * MM, hole_diameter)
|
SlotOverall(20 * MM, hole_diameter)
|
||||||
|
|
@ -262,7 +262,7 @@ or if variations of the part are needed.
|
||||||
|
|
||||||
*The dimensions of the bracket are defined as follows:*
|
*The dimensions of the bracket are defined as follows:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
thickness = 3 * MM
|
thickness = 3 * MM
|
||||||
width = 25 * MM
|
width = 25 * MM
|
||||||
|
|
@ -285,7 +285,7 @@ These steps should guide you through a logical and efficient workflow in build12
|
||||||
|
|
||||||
*The entire code block for the bracket example is shown here:*
|
*The entire code block for the bracket example is shown here:*
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: build123d
|
||||||
|
|
||||||
from build123d import *
|
from build123d import *
|
||||||
from ocp_vscode import show_all
|
from ocp_vscode import show_all
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ Before getting to the CAD operations, this selector script needs to import the b
|
||||||
environment.
|
environment.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [import]
|
:start-after: [import]
|
||||||
:end-before: [Hinge Class]
|
:end-before: [Hinge Class]
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ tutorial is the joints and not the CAD operations to create objects, this code i
|
||||||
described in detail.
|
described in detail.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Hinge Class]
|
:start-after: [Hinge Class]
|
||||||
:end-before: [Create the Joints]
|
:end-before: [Create the Joints]
|
||||||
|
|
||||||
|
|
@ -62,6 +64,7 @@ The first joint to add is a :class:`~topology.RigidJoint` that is used to fix th
|
||||||
or lid.
|
or lid.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Create the Joints]
|
:start-after: [Create the Joints]
|
||||||
:end-before: [Hinge Axis]
|
:end-before: [Hinge Axis]
|
||||||
|
|
||||||
|
|
@ -78,6 +81,7 @@ The second joint to add is either a :class:`~topology.RigidJoint` (on the inner
|
||||||
(on the outer leaf) that describes the hinge axis.
|
(on the outer leaf) that describes the hinge axis.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Create the Joints]
|
:start-after: [Create the Joints]
|
||||||
:end-before: [Fastener holes]
|
:end-before: [Fastener holes]
|
||||||
:emphasize-lines: 10-24
|
:emphasize-lines: 10-24
|
||||||
|
|
@ -96,6 +100,7 @@ The third set of joints to add are :class:`~topology.CylindricalJoint`'s that de
|
||||||
screws used to attach the leaves move.
|
screws used to attach the leaves move.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Fastener holes]
|
:start-after: [Fastener holes]
|
||||||
:end-before: [End Fastener holes]
|
:end-before: [End Fastener holes]
|
||||||
|
|
||||||
|
|
@ -115,6 +120,7 @@ Step 3d: Call Super
|
||||||
To finish off, the base class for the Hinge class is initialized:
|
To finish off, the base class for the Hinge class is initialized:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [End Fastener holes]
|
:start-after: [End Fastener holes]
|
||||||
:end-before: [Hinge Class]
|
:end-before: [Hinge Class]
|
||||||
|
|
||||||
|
|
@ -125,6 +131,7 @@ Now that the Hinge class is complete it can be used to instantiate the two hinge
|
||||||
required to attach the box and lid together.
|
required to attach the box and lid together.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Create instances of the two leaves of the hinge]
|
:start-after: [Create instances of the two leaves of the hinge]
|
||||||
:end-before: [Create the box with a RigidJoint to mount the hinge]
|
:end-before: [Create the box with a RigidJoint to mount the hinge]
|
||||||
|
|
||||||
|
|
@ -139,6 +146,7 @@ the joint used to attach the outer hinge leaf.
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Create the box with a RigidJoint to mount the hinge]
|
:start-after: [Create the box with a RigidJoint to mount the hinge]
|
||||||
:end-before: [Demonstrate that objects with Joints can be moved and the joints follow]
|
:end-before: [Demonstrate that objects with Joints can be moved and the joints follow]
|
||||||
:emphasize-lines: 13-16
|
:emphasize-lines: 13-16
|
||||||
|
|
@ -157,6 +165,7 @@ having to recreate or modify :class:`~topology.Joint`'s. Here is the box is move
|
||||||
property.
|
property.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Demonstrate that objects with Joints can be moved and the joints follow]
|
:start-after: [Demonstrate that objects with Joints can be moved and the joints follow]
|
||||||
:end-before: [The lid with a RigidJoint for the hinge]
|
:end-before: [The lid with a RigidJoint for the hinge]
|
||||||
|
|
||||||
|
|
@ -170,6 +179,7 @@ Much like the box, the lid is created in a :class:`~build_part.BuildPart` contex
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [The lid with a RigidJoint for the hinge]
|
:start-after: [The lid with a RigidJoint for the hinge]
|
||||||
:end-before: [A screw to attach the hinge to the box]
|
:end-before: [A screw to attach the hinge to the box]
|
||||||
:emphasize-lines: 6-9
|
:emphasize-lines: 6-9
|
||||||
|
|
@ -191,6 +201,7 @@ screw.
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [A screw to attach the hinge to the box]
|
:start-after: [A screw to attach the hinge to the box]
|
||||||
:end-before: [End of screw creation]
|
:end-before: [End of screw creation]
|
||||||
|
|
||||||
|
|
@ -210,6 +221,7 @@ Step 7a: Hinge to Box
|
||||||
To start, the outer hinge leaf will be connected to the box, as follows:
|
To start, the outer hinge leaf will be connected to the box, as follows:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Connect Box to Outer Hinge]
|
:start-after: [Connect Box to Outer Hinge]
|
||||||
:end-before: [Connect Box to Outer Hinge]
|
:end-before: [Connect Box to Outer Hinge]
|
||||||
|
|
||||||
|
|
@ -227,6 +239,7 @@ Next, the hinge inner leaf is connected to the hinge outer leaf which is attache
|
||||||
box.
|
box.
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Connect Hinge Leaves]
|
:start-after: [Connect Hinge Leaves]
|
||||||
:end-before: [Connect Hinge Leaves]
|
:end-before: [Connect Hinge Leaves]
|
||||||
|
|
||||||
|
|
@ -243,6 +256,7 @@ Step 7c: Lid to Hinge
|
||||||
Now the ``lid`` is connected to the ``hinge_inner``:
|
Now the ``lid`` is connected to the ``hinge_inner``:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Connect Hinge to Lid]
|
:start-after: [Connect Hinge to Lid]
|
||||||
:end-before: [Connect Hinge to Lid]
|
:end-before: [Connect Hinge to Lid]
|
||||||
|
|
||||||
|
|
@ -260,6 +274,7 @@ Step 7d: Screw to Hinge
|
||||||
The last step in this example is to place a screw in one of the hinges:
|
The last step in this example is to place a screw in one of the hinges:
|
||||||
|
|
||||||
.. literalinclude:: tutorial_joints.py
|
.. literalinclude:: tutorial_joints.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Connect Screw to Hole]
|
:start-after: [Connect Screw to Hole]
|
||||||
:end-before: [Connect Screw to Hole]
|
:end-before: [Connect Screw to Hole]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ The dimensions of the Lego block follow. A key parameter is ``pip_count``, the l
|
||||||
of the Lego blocks in pips. This parameter must be at least 2.
|
of the Lego blocks in pips. This parameter must be at least 2.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 30,31, 34-47
|
:lines: 30,31, 34-47
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
@ -31,6 +32,7 @@ The Lego block will be created by the ``BuildPart`` builder as it's a discrete t
|
||||||
dimensional part; therefore, we'll instantiate a ``BuildPart`` with the name ``lego``.
|
dimensional part; therefore, we'll instantiate a ``BuildPart`` with the name ``lego``.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49
|
:lines: 49
|
||||||
|
|
||||||
**********************
|
**********************
|
||||||
|
|
@ -43,6 +45,7 @@ object. As this sketch will be part of the lego part, we'll create a sketch bui
|
||||||
in the context of the part builder as follows:
|
in the context of the part builder as follows:
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-51
|
:lines: 49-51
|
||||||
:emphasize-lines: 3
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
|
@ -59,6 +62,7 @@ of the Lego block. The following step is going to refer to this rectangle, so it
|
||||||
be assigned the identifier ``perimeter``.
|
be assigned the identifier ``perimeter``.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53
|
:lines: 49-53
|
||||||
:emphasize-lines: 5
|
:emphasize-lines: 5
|
||||||
|
|
||||||
|
|
@ -76,6 +80,7 @@ hollowed out. This will be done with the ``Offset`` operation which is going to
|
||||||
create a new object from ``perimeter``.
|
create a new object from ``perimeter``.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64
|
:lines: 49-53,58-64
|
||||||
:emphasize-lines: 7-12
|
:emphasize-lines: 7-12
|
||||||
|
|
||||||
|
|
@ -104,6 +109,7 @@ objects are in the scope of a location context (``GridLocations`` in this case)
|
||||||
that defined multiple points, multiple rectangles are created.
|
that defined multiple points, multiple rectangles are created.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73
|
:lines: 49-53,58-64,69-73
|
||||||
:emphasize-lines: 13-17
|
:emphasize-lines: 13-17
|
||||||
|
|
||||||
|
|
@ -125,6 +131,7 @@ To convert the internal grid to ridges, the center needs to be removed. This wil
|
||||||
with another ``Rectangle``.
|
with another ``Rectangle``.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73,78-83
|
:lines: 49-53,58-64,69-73,78-83
|
||||||
:emphasize-lines: 18-23
|
:emphasize-lines: 18-23
|
||||||
|
|
||||||
|
|
@ -142,6 +149,7 @@ Lego blocks use a set of internal hollow cylinders that the pips push against
|
||||||
to hold two blocks together. These will be created with ``Circle``.
|
to hold two blocks together. These will be created with ``Circle``.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73,78-83,88-93
|
:lines: 49-53,58-64,69-73,78-83,88-93
|
||||||
:emphasize-lines: 24-29
|
:emphasize-lines: 24-29
|
||||||
|
|
||||||
|
|
@ -162,6 +170,7 @@ Now that the sketch is complete it needs to be extruded into the three dimension
|
||||||
wall object.
|
wall object.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99
|
:lines: 49-53,58-64,69-73,78-83,88-93,98-99
|
||||||
:emphasize-lines: 30-31
|
:emphasize-lines: 30-31
|
||||||
|
|
||||||
|
|
@ -183,6 +192,7 @@ Now that the walls are complete, the top of the block needs to be added. Althoug
|
||||||
could be done with another sketch, we'll add a box to the top of the walls.
|
could be done with another sketch, we'll add a box to the top of the walls.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118
|
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118
|
||||||
:emphasize-lines: 32-40
|
:emphasize-lines: 32-40
|
||||||
|
|
||||||
|
|
@ -211,6 +221,7 @@ The final step is to add the pips to the top of the Lego block. To do this we'll
|
||||||
a new workplane on top of the block where we can position the pips.
|
a new workplane on top of the block where we can position the pips.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/lego.py
|
.. literalinclude:: ../examples/lego.py
|
||||||
|
:language: build123d
|
||||||
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137
|
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137
|
||||||
:emphasize-lines: 41-49
|
:emphasize-lines: 41-49
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Before getting to the CAD operations, this selector script needs to import the b
|
||||||
environment.
|
environment.
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-2
|
:lines: 1-2
|
||||||
|
|
@ -34,6 +35,7 @@ To start off, the part will be based on a cylinder so we'll use the :class:`~obj
|
||||||
of :class:`~build_part.BuildPart`:
|
of :class:`~build_part.BuildPart`:
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-5
|
:lines: 1-5
|
||||||
|
|
@ -50,6 +52,7 @@ surfaces) , so we'll create a sketch centered on the top of the cylinder. To lo
|
||||||
this sketch we'll use the cylinder's top Face as shown here:
|
this sketch we'll use the cylinder's top Face as shown here:
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-6
|
:lines: 1-6
|
||||||
|
|
@ -82,6 +85,7 @@ The object has a hexagonal hole in the top with a central cylinder which we'll d
|
||||||
in the sketch.
|
in the sketch.
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-8
|
:lines: 1-8
|
||||||
|
|
@ -107,6 +111,7 @@ To create the hole we'll :func:`~operations_part.extrude` the sketch we just cre
|
||||||
the :class:`~objects_part.Cylinder` and subtract it.
|
the :class:`~objects_part.Cylinder` and subtract it.
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-9
|
:lines: 1-9
|
||||||
|
|
@ -128,6 +133,7 @@ Step 6: Fillet the top perimeter Edge
|
||||||
The final step is to apply a fillet to the top perimeter.
|
The final step is to apply a fillet to the top perimeter.
|
||||||
|
|
||||||
.. literalinclude:: selector_example.py
|
.. literalinclude:: selector_example.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
:lines: 1-9,18-24,33-34
|
:lines: 1-9,18-24,33-34
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ We model a single wing (half‑span), with an elliptic leading and trailing edge
|
||||||
These two edges act as the *guides* for the Gordon surface.
|
These two edges act as the *guides* for the Gordon surface.
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [AirfoilSizes]
|
:end-before: [AirfoilSizes]
|
||||||
|
|
||||||
|
|
@ -45,6 +46,7 @@ We intersect the guides with planes normal to the span to size the airfoil secti
|
||||||
The resulting chord lengths define uniform scales for each airfoil curve.
|
The resulting chord lengths define uniform scales for each airfoil curve.
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [AirfoilSizes]
|
:start-after: [AirfoilSizes]
|
||||||
:end-before: [Airfoils]
|
:end-before: [Airfoils]
|
||||||
|
|
||||||
|
|
@ -56,6 +58,7 @@ shifted so the leading edge fraction is aligned—then scale to the chord length
|
||||||
from Step 2.
|
from Step 2.
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Airfoils]
|
:start-after: [Airfoils]
|
||||||
:end-before: [Profiles]
|
:end-before: [Profiles]
|
||||||
|
|
||||||
|
|
@ -68,6 +71,7 @@ profiles; the elliptic edges are the guides. We also add the wing tip section
|
||||||
so the profile grid closes at the tip.
|
so the profile grid closes at the tip.
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Profiles]
|
:start-after: [Profiles]
|
||||||
:end-before: [Solid]
|
:end-before: [Solid]
|
||||||
|
|
||||||
|
|
@ -82,6 +86,7 @@ Step 5 — Cap the root and create the solid
|
||||||
We extract the closed root edge loop, make a planar cap, and form a solid shell.
|
We extract the closed root edge loop, make a planar cap, and form a solid shell.
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Solid]
|
:start-after: [Solid]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -102,5 +107,6 @@ Complete listing
|
||||||
For convenience, here is the full script in one block:
|
For convenience, here is the full script in one block:
|
||||||
|
|
||||||
.. literalinclude:: spitfire_wing_gordon.py
|
.. literalinclude:: spitfire_wing_gordon.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ the object. To illustrate this process, we will create the following game token:
|
||||||
Useful :class:`~topology.Face` creation methods include
|
Useful :class:`~topology.Face` creation methods include
|
||||||
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
|
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
|
||||||
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
|
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
|
||||||
:doc:`surface_modeling` overview for the full list.
|
:doc:`tutorial_surface_modeling` overview for the full list.
|
||||||
|
|
||||||
In this case, we'll use the ``make_surface`` method, providing it with the edges that define
|
In this case, we'll use the ``make_surface`` method, providing it with the edges that define
|
||||||
the perimeter of the surface and a central point on that surface.
|
the perimeter of the surface and a central point on that surface.
|
||||||
|
|
@ -29,6 +29,7 @@ To create the perimeter, we'll define the perimeter edges. Since the heart is
|
||||||
symmetric, we'll only create half of its surface here:
|
symmetric, we'll only create half of its surface here:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Code]
|
:start-after: [Code]
|
||||||
:end-before: [SurfaceEdges]
|
:end-before: [SurfaceEdges]
|
||||||
|
|
||||||
|
|
@ -42,12 +43,14 @@ of the heart and archs up off ``Plane.XY``.
|
||||||
In preparation for creating the surface, we'll define a point on the surface:
|
In preparation for creating the surface, we'll define a point on the surface:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [SurfaceEdges]
|
:start-after: [SurfaceEdges]
|
||||||
:end-before: [SurfacePoint]
|
:end-before: [SurfacePoint]
|
||||||
|
|
||||||
We will then use this point to create a non-planar ``Face``:
|
We will then use this point to create a non-planar ``Face``:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [SurfacePoint]
|
:start-after: [SurfacePoint]
|
||||||
:end-before: [Surface]
|
:end-before: [Surface]
|
||||||
|
|
||||||
|
|
@ -63,6 +66,7 @@ Now that one half of the top of the heart has been created, the remainder of the
|
||||||
and bottom can be created by mirroring:
|
and bottom can be created by mirroring:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Surface]
|
:start-after: [Surface]
|
||||||
:end-before: [Surfaces]
|
:end-before: [Surfaces]
|
||||||
|
|
||||||
|
|
@ -70,6 +74,7 @@ The sides of the heart are going to be created by extruding the outside of the p
|
||||||
as follows:
|
as follows:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Surfaces]
|
:start-after: [Surfaces]
|
||||||
:end-before: [Sides]
|
:end-before: [Sides]
|
||||||
|
|
||||||
|
|
@ -82,6 +87,7 @@ now put them together, first into a :class:`~topology.Shell` and then into a
|
||||||
:class:`~topology.Solid`:
|
:class:`~topology.Solid`:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Sides]
|
:start-after: [Sides]
|
||||||
:end-before: [Solid]
|
:end-before: [Solid]
|
||||||
|
|
||||||
|
|
@ -99,6 +105,7 @@ Finally, we'll create the frame around the heart as a simple extrusion of a plan
|
||||||
shape defined by the perimeter of the heart and merge all of the components together:
|
shape defined by the perimeter of the heart and merge all of the components together:
|
||||||
|
|
||||||
.. literalinclude:: heart_token.py
|
.. literalinclude:: heart_token.py
|
||||||
|
:language: build123d
|
||||||
:start-after: [Solid]
|
:start-after: [Solid]
|
||||||
:end-before: [End]
|
:end-before: [End]
|
||||||
|
|
||||||
|
|
@ -121,5 +128,5 @@ from the heart.
|
||||||
Next steps
|
Next steps
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Continue to :doc:`tutorial_heart_token` for an advanced example using
|
Continue to :doc:`tutorial_spitfire_wing_gordon` for an advanced example using
|
||||||
:meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing.
|
:meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing.
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,7 @@ with BuildPart() as ex26:
|
||||||
with BuildSketch() as ex26_sk:
|
with BuildSketch() as ex26_sk:
|
||||||
with Locations((0, rev)):
|
with Locations((0, rev)):
|
||||||
Circle(rad)
|
Circle(rad)
|
||||||
revolve(axis=Axis.X, revolution_arc=90)
|
revolve(axis=Axis.X, revolution_arc=180)
|
||||||
mirror(about=Plane.XZ)
|
|
||||||
with BuildSketch() as ex26_sk2:
|
with BuildSketch() as ex26_sk2:
|
||||||
Rectangle(rad, rev)
|
Rectangle(rad, rev)
|
||||||
ex26_target = ex26.part
|
ex26_target = ex26.part
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ rad, rev = 3, 25
|
||||||
|
|
||||||
# Extrude last
|
# Extrude last
|
||||||
circle = Pos(0, rev) * Circle(rad)
|
circle = Pos(0, rev) * Circle(rad)
|
||||||
ex26_target = revolve(circle, Axis.X, revolution_arc=90)
|
ex26_target = revolve(circle, Axis.X, revolution_arc=180)
|
||||||
ex26_target = ex26_target + mirror(ex26_target, Plane.XZ)
|
ex26_target = ex26_target
|
||||||
|
|
||||||
rect = Rectangle(rad, rev)
|
rect = Rectangle(rad, rev)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ development = [
|
||||||
"black",
|
"black",
|
||||||
"mypy",
|
"mypy",
|
||||||
"pylint",
|
"pylint",
|
||||||
"pytest",
|
"pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
|
||||||
"pytest-benchmark",
|
"pytest-benchmark",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
|
||||||
elif mode == Mode.INTERSECT:
|
elif mode == Mode.INTERSECT:
|
||||||
if self._obj is None:
|
if self._obj is None:
|
||||||
raise RuntimeError("Nothing to intersect with")
|
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:
|
elif mode == Mode.REPLACE:
|
||||||
combined = self._sub_class(list(typed[self._shape]))
|
combined = self._sub_class(list(typed[self._shape]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
|
||||||
if self_intersection is None:
|
if self_intersection is None:
|
||||||
self_intersection_area = 0.0
|
self_intersection_area = 0.0
|
||||||
else:
|
else:
|
||||||
self_intersection_area = self_intersection.area
|
self_intersection_area = sum(f.area for f in self_intersection.faces())
|
||||||
d_line += placed_label
|
d_line += placed_label
|
||||||
bbox_size = d_line.bounding_box().diagonal
|
bbox_size = d_line.bounding_box().diagonal
|
||||||
|
|
||||||
|
|
@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
|
||||||
if line_intersection is None:
|
if line_intersection is None:
|
||||||
common_area = 0.0
|
common_area = 0.0
|
||||||
else:
|
else:
|
||||||
common_area = line_intersection.area
|
common_area = sum(f.area for f in line_intersection.faces())
|
||||||
common_area += self_intersection_area
|
common_area += self_intersection_area
|
||||||
score = (d_line.area - 10 * common_area) / bbox_size
|
score = (d_line.area - 10 * common_area) / bbox_size
|
||||||
d_lines[d_line] = score
|
d_lines[d_line] = score
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,10 @@ import math
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from io import BytesIO
|
||||||
from os import PathLike, fsdecode
|
from os import PathLike, fsdecode
|
||||||
from typing import Any, TypeAlias
|
from typing import Any, TypeAlias
|
||||||
|
from typing import cast as tcast
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
|
@ -47,7 +49,7 @@ from ezdxf.colors import RGB, aci2rgb
|
||||||
from ezdxf.math import Vec2
|
from ezdxf.math import Vec2
|
||||||
from OCP.BRepLib import BRepLib
|
from OCP.BRepLib import BRepLib
|
||||||
from OCP.BRepTools import BRepTools_WireExplorer
|
from OCP.BRepTools import BRepTools_WireExplorer
|
||||||
from OCP.Geom import Geom_BezierCurve
|
from OCP.Geom import Geom_BezierCurve, Geom_BSplineCurve
|
||||||
from OCP.GeomConvert import GeomConvert
|
from OCP.GeomConvert import GeomConvert
|
||||||
from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve
|
from OCP.GeomConvert import GeomConvert_BSplineCurveToBezierCurve
|
||||||
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ
|
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec, gp_XYZ
|
||||||
|
|
@ -636,13 +638,13 @@ class ExportDXF(Export2D):
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
def write(self, file_name: PathLike | str | bytes):
|
def write(self, file_name: PathLike | str | bytes | BytesIO):
|
||||||
"""write
|
"""write
|
||||||
|
|
||||||
Writes the DXF data to the specified file name.
|
Writes the DXF data to the specified file name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_name (PathLike | str | bytes): The file name (including path) where
|
file_name (PathLike | str | bytes | BytesIO): The file name (including path) where
|
||||||
the DXF data will be written.
|
the DXF data will be written.
|
||||||
"""
|
"""
|
||||||
# Reset the main CAD viewport of the model space to the
|
# Reset the main CAD viewport of the model space to the
|
||||||
|
|
@ -650,7 +652,12 @@ class ExportDXF(Export2D):
|
||||||
# https://github.com/gumyr/build123d/issues/382 tracks
|
# https://github.com/gumyr/build123d/issues/382 tracks
|
||||||
# exposing viewport control to the user.
|
# exposing viewport control to the user.
|
||||||
zoom.extents(self._modelspace)
|
zoom.extents(self._modelspace)
|
||||||
self._document.saveas(fsdecode(file_name))
|
|
||||||
|
if not isinstance(file_name, BytesIO):
|
||||||
|
file_name = fsdecode(file_name)
|
||||||
|
self._document.saveas(file_name)
|
||||||
|
else:
|
||||||
|
self._document.write(file_name, fmt="bin")
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
|
@ -751,14 +758,14 @@ class ExportDXF(Export2D):
|
||||||
|
|
||||||
# Extract the relevant segment of the curve.
|
# Extract the relevant segment of the curve.
|
||||||
spline = GeomConvert.SplitBSplineCurve_s(
|
spline = GeomConvert.SplitBSplineCurve_s(
|
||||||
curve,
|
tcast(Geom_BSplineCurve, curve),
|
||||||
u1,
|
u1,
|
||||||
u2,
|
u2,
|
||||||
Export2D.PARAMETRIC_TOLERANCE,
|
Export2D.PARAMETRIC_TOLERANCE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# need to apply the transform on the geometry level
|
# 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}.")
|
raise ValueError(f"Edge is empty {edge}.")
|
||||||
t = edge.location.wrapped.Transformation()
|
t = edge.location.wrapped.Transformation()
|
||||||
spline.Transform(t)
|
spline.Transform(t)
|
||||||
|
|
@ -1130,7 +1137,7 @@ class ExportSVG(Export2D):
|
||||||
)
|
)
|
||||||
while explorer.More():
|
while explorer.More():
|
||||||
topo_edge = explorer.Current()
|
topo_edge = explorer.Current()
|
||||||
loose_edges.append(Edge(topo_edge))
|
loose_edges.append(Edge(TopoDS.Edge_s(topo_edge)))
|
||||||
explorer.Next()
|
explorer.Next()
|
||||||
# print(f"{len(loose_edges)} loose edges")
|
# print(f"{len(loose_edges)} loose edges")
|
||||||
loose_edge_elements = [self._edge_element(edge) for edge in loose_edges]
|
loose_edge_elements = [self._edge_element(edge) for edge in loose_edges]
|
||||||
|
|
@ -1257,7 +1264,7 @@ class ExportSVG(Export2D):
|
||||||
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
||||||
start = self._path_point(curve.Value(u0))
|
start = self._path_point(curve.Value(u0))
|
||||||
end = self._path_point(curve.Value(u1))
|
end = self._path_point(curve.Value(u1))
|
||||||
radius = complex(radius, radius)
|
radius = complex(radius, radius) # type: ignore[assignment]
|
||||||
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
||||||
if curve.IsClosed():
|
if curve.IsClosed():
|
||||||
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
||||||
|
|
@ -1310,7 +1317,7 @@ class ExportSVG(Export2D):
|
||||||
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
(u0, u1) = (lp, fp) if reverse else (fp, lp)
|
||||||
start = self._path_point(curve.Value(u0))
|
start = self._path_point(curve.Value(u0))
|
||||||
end = self._path_point(curve.Value(u1))
|
end = self._path_point(curve.Value(u1))
|
||||||
radius = complex(major_radius, minor_radius)
|
radius = complex(major_radius, minor_radius) # type: ignore[assignment]
|
||||||
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
rotation = math.degrees(gp_Dir(1, 0, 0).AngleWithRef(x_axis, gp_Dir(0, 0, 1)))
|
||||||
if curve.IsClosed():
|
if curve.IsClosed():
|
||||||
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
midway = self._path_point(curve.Value((u0 + u1) / 2))
|
||||||
|
|
@ -1345,7 +1352,7 @@ class ExportSVG(Export2D):
|
||||||
u2 = adaptor.LastParameter()
|
u2 = adaptor.LastParameter()
|
||||||
|
|
||||||
# Apply the shape location to the geometry.
|
# 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}.")
|
raise ValueError(f"Edge is empty {edge}.")
|
||||||
t = edge.location.wrapped.Transformation()
|
t = edge.location.wrapped.Transformation()
|
||||||
spline.Transform(t)
|
spline.Transform(t)
|
||||||
|
|
@ -1355,7 +1362,7 @@ class ExportSVG(Export2D):
|
||||||
# According to the OCCT 7.6.0 documentation,
|
# According to the OCCT 7.6.0 documentation,
|
||||||
# "ParametricTolerance is not used."
|
# "ParametricTolerance is not used."
|
||||||
converter = GeomConvert_BSplineCurveToBezierCurve(
|
converter = GeomConvert_BSplineCurveToBezierCurve(
|
||||||
spline, u1, u2, Export2D.PARAMETRIC_TOLERANCE
|
tcast(Geom_BSplineCurve, spline), u1, u2, Export2D.PARAMETRIC_TOLERANCE
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_segment(bezier: Geom_BezierCurve, reverse: bool) -> PathSegment:
|
def make_segment(bezier: Geom_BezierCurve, reverse: bool) -> PathSegment:
|
||||||
|
|
@ -1411,7 +1418,7 @@ class ExportSVG(Export2D):
|
||||||
}
|
}
|
||||||
|
|
||||||
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
|
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}.")
|
raise ValueError(f"Edge is empty {edge}.")
|
||||||
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
|
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
|
||||||
geom_type = edge.geom_type
|
geom_type = edge.geom_type
|
||||||
|
|
@ -1497,13 +1504,13 @@ class ExportSVG(Export2D):
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
def write(self, path: PathLike | str | bytes):
|
def write(self, path: PathLike | str | bytes | BytesIO):
|
||||||
"""write
|
"""write
|
||||||
|
|
||||||
Writes the SVG data to the specified file path.
|
Writes the SVG data to the specified file path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (PathLike | str | bytes): The file path where the SVG data will be written.
|
path (PathLike | str | bytes | BytesIO): The file path where the SVG data will be written.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
bb = self._bounds
|
bb = self._bounds
|
||||||
|
|
@ -1549,5 +1556,9 @@ class ExportSVG(Export2D):
|
||||||
|
|
||||||
xml = ET.ElementTree(svg)
|
xml = ET.ElementTree(svg)
|
||||||
ET.indent(xml, " ")
|
ET.indent(xml, " ")
|
||||||
|
|
||||||
|
if not isinstance(path, BytesIO):
|
||||||
|
path = fsdecode(path)
|
||||||
|
|
||||||
# xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False)
|
# xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False)
|
||||||
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None)
|
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None)
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ def export_gltf(
|
||||||
|
|
||||||
messenger = Message.DefaultMessenger_s()
|
messenger = Message.DefaultMessenger_s()
|
||||||
for printer in messenger.Printers():
|
for printer in messenger.Printers():
|
||||||
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail))
|
printer.SetTraceLevel(Message_Gravity.Message_Fail)
|
||||||
|
|
||||||
status = writer.Perform(doc, index_map, progress)
|
status = writer.Perform(doc, index_map, progress)
|
||||||
|
|
||||||
|
|
@ -262,7 +262,7 @@ def export_gltf(
|
||||||
|
|
||||||
def export_step(
|
def export_step(
|
||||||
to_export: Shape,
|
to_export: Shape,
|
||||||
file_path: PathLike | str | bytes,
|
file_path: PathLike | str | bytes | BytesIO,
|
||||||
unit: Unit = Unit.MM,
|
unit: Unit = Unit.MM,
|
||||||
write_pcurves: bool = True,
|
write_pcurves: bool = True,
|
||||||
precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
|
precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
|
||||||
|
|
@ -277,7 +277,7 @@ def export_step(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to_export (Shape): object or assembly
|
to_export (Shape): object or assembly
|
||||||
file_path (Union[PathLike, str, bytes]): step file path
|
file_path (Union[PathLike, str, bytes, BytesIO]): step file path
|
||||||
unit (Unit, optional): shape units. Defaults to Unit.MM.
|
unit (Unit, optional): shape units. Defaults to Unit.MM.
|
||||||
write_pcurves (bool, optional): write parametric curves to the STEP file.
|
write_pcurves (bool, optional): write parametric curves to the STEP file.
|
||||||
Defaults to True.
|
Defaults to True.
|
||||||
|
|
@ -297,7 +297,7 @@ def export_step(
|
||||||
# Disable writing OCCT info to console
|
# Disable writing OCCT info to console
|
||||||
messenger = Message.DefaultMessenger_s()
|
messenger = Message.DefaultMessenger_s()
|
||||||
for printer in messenger.Printers():
|
for printer in messenger.Printers():
|
||||||
printer.SetTraceLevel(Message_Gravity(Message_Gravity.Message_Fail))
|
printer.SetTraceLevel(Message_Gravity.Message_Fail)
|
||||||
|
|
||||||
session = XSControl_WorkSession()
|
session = XSControl_WorkSession()
|
||||||
writer = STEPCAFControl_Writer(session, False)
|
writer = STEPCAFControl_Writer(session, False)
|
||||||
|
|
@ -326,7 +326,13 @@ def export_step(
|
||||||
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
|
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
|
||||||
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
|
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
|
||||||
|
|
||||||
status = writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
|
if not isinstance(file_path, BytesIO):
|
||||||
|
status = (
|
||||||
|
writer.Write(fsdecode(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status = writer.WriteStream(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
raise RuntimeError("Failed to write STEP file")
|
raise RuntimeError("Failed to write STEP file")
|
||||||
|
|
||||||
|
|
@ -346,7 +352,7 @@ def export_stl(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to_export (Shape): object or assembly
|
to_export (Shape): object or assembly
|
||||||
file_path (str): The path and file name to write the STL output to.
|
file_path (Union[PathLike, str, bytes]): The path and file name to write the STL output to.
|
||||||
tolerance (float, optional): A linear deflection setting which limits the distance
|
tolerance (float, optional): A linear deflection setting which limits the distance
|
||||||
between a curve and its tessellation. Setting this value too low will result in
|
between a curve and its tessellation. Setting this value too low will result in
|
||||||
large meshes that can consume computing resources. Setting the value too high can
|
large meshes that can consume computing resources. Setting the value too high can
|
||||||
|
|
@ -368,7 +374,4 @@ def export_stl(
|
||||||
writer = StlAPI_Writer()
|
writer = StlAPI_Writer()
|
||||||
|
|
||||||
writer.ASCIIMode = ascii_format
|
writer.ASCIIMode = ascii_format
|
||||||
|
return writer.Write(to_export.wrapped, fsdecode(file_path))
|
||||||
file_path = str(file_path)
|
|
||||||
|
|
||||||
return writer.Write(to_export.wrapped, file_path)
|
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,14 @@ from __future__ import annotations
|
||||||
# other pylint warning to temp remove:
|
# other pylint warning to temp remove:
|
||||||
# too-many-arguments, too-many-locals, too-many-public-methods,
|
# too-many-arguments, too-many-locals, too-many-public-methods,
|
||||||
# too-many-statements, too-many-instance-attributes, too-many-branches
|
# too-many-statements, too-many-instance-attributes, too-many-branches
|
||||||
|
import colorsys
|
||||||
import copy as copy_module
|
import copy as copy_module
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Callable, Iterable, Sequence
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from math import degrees, isclose, log10, pi, radians
|
from math import degrees, isclose, log10, pi, radians, prod
|
||||||
from typing import TYPE_CHECKING, Any, TypeAlias, overload
|
from typing import TYPE_CHECKING, Any, TypeAlias, overload
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -1000,6 +1001,16 @@ class BoundBox:
|
||||||
self.max = Vector(x_max, y_max, z_max) #: location of maximum corner
|
self.max = Vector(x_max, y_max, z_max) #: location of maximum corner
|
||||||
self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min) #: overall size
|
self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min) #: overall size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def measure(self) -> float:
|
||||||
|
"""Return the overall Lebesgue measure of the bounding box.
|
||||||
|
|
||||||
|
- For 1D objects: length
|
||||||
|
- For 2D objects: area
|
||||||
|
- For 3D objects: volume
|
||||||
|
"""
|
||||||
|
return prod([x for x in self.size if x > TOLERANCE])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def diagonal(self) -> float:
|
def diagonal(self) -> float:
|
||||||
"""body diagonal length (i.e. object maximum size)"""
|
"""body diagonal length (i.e. object maximum size)"""
|
||||||
|
|
@ -1160,8 +1171,8 @@ class Color:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
color_like (ColorLike):
|
color_like (ColorLike):
|
||||||
name, ex: "red",
|
name, ex: "red" or "#ff0000",
|
||||||
name + alpha, ex: ("red", 0.5),
|
name + alpha, ex: ("red", 0.5) or "#ff000080",
|
||||||
rgb, ex: (1., 0., 0.),
|
rgb, ex: (1., 0., 0.),
|
||||||
rgb + alpha, ex: (1., 0., 0., 0.5),
|
rgb + alpha, ex: (1., 0., 0., 0.5),
|
||||||
hex, ex: 0xff0000,
|
hex, ex: 0xff0000,
|
||||||
|
|
@ -1172,7 +1183,7 @@ class Color:
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, name: str, alpha: float = 1.0):
|
def __init__(self, name: str, alpha: float = 1.0):
|
||||||
"""Color from name
|
"""Color from name or hexadecimal string
|
||||||
|
|
||||||
`CSS3 Color Names
|
`CSS3 Color Names
|
||||||
<https://en.wikipedia.org/wiki/Web_colors#Extended_colors>`
|
<https://en.wikipedia.org/wiki/Web_colors#Extended_colors>`
|
||||||
|
|
@ -1180,8 +1191,10 @@ class Color:
|
||||||
`OCCT Color Names
|
`OCCT Color Names
|
||||||
<https://dev.opencascade.org/doc/refman/html/_quantity___name_of_color_8hxx.html>`_
|
<https://dev.opencascade.org/doc/refman/html/_quantity___name_of_color_8hxx.html>`_
|
||||||
|
|
||||||
|
Hexadecimal string may be RGB or RGBA format with leading "#"
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): color, e.g. "blue"
|
name (str): color, e.g. "blue" or "#0000ff""
|
||||||
alpha (float, optional): 0.0 <= alpha <= 1.0. Defaults to 1.0
|
alpha (float, optional): 0.0 <= alpha <= 1.0. Defaults to 1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -1237,6 +1250,27 @@ class Color:
|
||||||
return
|
return
|
||||||
case str():
|
case str():
|
||||||
name, alpha = fill_defaults(args, (name, alpha))
|
name, alpha = fill_defaults(args, (name, alpha))
|
||||||
|
name = name.strip()
|
||||||
|
if "#" in name:
|
||||||
|
# extract alpha from hex string
|
||||||
|
hex_a = format(int(alpha * 255), "x")
|
||||||
|
if len(name) == 5:
|
||||||
|
hex_a = name[4] * 2
|
||||||
|
name = name[:4]
|
||||||
|
elif len(name) == 9:
|
||||||
|
hex_a = name[7:9]
|
||||||
|
name = name[:7]
|
||||||
|
elif len(name) not in [4, 5, 7, 9]:
|
||||||
|
raise ValueError(
|
||||||
|
f'"{name}" is not a valid hexadecimal color value.'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if hex_a:
|
||||||
|
alpha = int(hex_a, 16) / 0xFF
|
||||||
|
except ValueError as ex:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invald alpha hex string: {hex_a}"
|
||||||
|
) from ex
|
||||||
case int():
|
case int():
|
||||||
color_code, alpha = fill_defaults(args, (color_code, alpha))
|
color_code, alpha = fill_defaults(args, (color_code, alpha))
|
||||||
case float():
|
case float():
|
||||||
|
|
@ -1296,16 +1330,6 @@ class Color:
|
||||||
self.iter_index += 1
|
self.iter_index += 1
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def to_tuple(self):
|
|
||||||
"""Value as tuple"""
|
|
||||||
warnings.warn(
|
|
||||||
"to_tuple is deprecated and will be removed in a future version. "
|
|
||||||
"Use 'tuple(Color)' instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
return tuple(self)
|
|
||||||
|
|
||||||
def __copy__(self) -> Color:
|
def __copy__(self) -> Color:
|
||||||
"""Return copy of self"""
|
"""Return copy of self"""
|
||||||
return Color(*tuple(self))
|
return Color(*tuple(self))
|
||||||
|
|
@ -1332,6 +1356,74 @@ class Color:
|
||||||
"""Color repr"""
|
"""Color repr"""
|
||||||
return f"Color{str(tuple(self))}"
|
return f"Color{str(tuple(self))}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def categorical_set(
|
||||||
|
cls,
|
||||||
|
color_count: int,
|
||||||
|
starting_hue: ColorLike | float = 0.0,
|
||||||
|
alpha: float | Iterable[float] = 1.0,
|
||||||
|
) -> list[Color]:
|
||||||
|
"""Generate a palette of evenly spaced colors.
|
||||||
|
|
||||||
|
Creates a list of visually distinct colors suitable for representing
|
||||||
|
discrete categories (such as different parts, assemblies, or data
|
||||||
|
series). Colors are evenly spaced around the hue circle and share
|
||||||
|
consistent lightness and saturation levels, resulting in balanced
|
||||||
|
perceptual contrast across all hues.
|
||||||
|
|
||||||
|
Produces palettes similar in appearance to the **Tableau 10** and **D3
|
||||||
|
Category10** color sets—both widely recognized standards in data
|
||||||
|
visualization for their clarity and accessibility. These values have
|
||||||
|
been empirically chosen to maintain consistent perceived brightness
|
||||||
|
across hues while avoiding overly vivid or dark colors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
color_count (int): Number of colors to generate.
|
||||||
|
starting_hue (ColorLike | float): Either a Color-like object or
|
||||||
|
a hue value in the range [0.0, 1.0] that defines the starting color.
|
||||||
|
alpha (float | Iterable[float]): Alpha value(s) for the colors. Can be a
|
||||||
|
single float or an iterable of length `color_count`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Color]: List of generated colors.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If starting_hue is out of range or alpha length mismatch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Determine starting hue ---
|
||||||
|
if isinstance(starting_hue, float):
|
||||||
|
if not (0.0 <= starting_hue <= 1.0):
|
||||||
|
raise ValueError("Starting hue must be within range 0.0–1.0")
|
||||||
|
elif isinstance(starting_hue, int):
|
||||||
|
if starting_hue < 0:
|
||||||
|
raise ValueError("Starting color integer must be non-negative")
|
||||||
|
rgb = tuple(Color(starting_hue))[:3]
|
||||||
|
starting_hue = colorsys.rgb_to_hls(*rgb)[0]
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"Starting hue must be a float in [0,1] or an integer color literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Normalize alpha values ---
|
||||||
|
if isinstance(alpha, (float, int)):
|
||||||
|
alphas = [float(alpha)] * color_count
|
||||||
|
else:
|
||||||
|
alphas = list(alpha)
|
||||||
|
if len(alphas) != color_count:
|
||||||
|
raise ValueError("Number of alpha values must match color_count")
|
||||||
|
|
||||||
|
# --- Generate color list ---
|
||||||
|
hues = np.linspace(
|
||||||
|
starting_hue, starting_hue + 1.0, color_count, endpoint=False
|
||||||
|
)
|
||||||
|
colors = [
|
||||||
|
cls(*colorsys.hls_to_rgb(h % 1.0, 0.55, 0.9), a)
|
||||||
|
for h, a in zip(hues, alphas)
|
||||||
|
]
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
|
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
|
||||||
red, remainder = divmod(triplet, 256**2)
|
red, remainder = divmod(triplet, 256**2)
|
||||||
|
|
@ -1340,7 +1432,6 @@ class Color:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rgb_from_str(name: str) -> tuple:
|
def _rgb_from_str(name: str) -> tuple:
|
||||||
name = name.strip()
|
|
||||||
if "#" not in name:
|
if "#" not in name:
|
||||||
try:
|
try:
|
||||||
# Use css3 color names by default
|
# Use css3 color names by default
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ license:
|
||||||
# pylint: disable=no-name-in-module, import-error
|
# pylint: disable=no-name-in-module, import-error
|
||||||
import copy as copy_module
|
import copy as copy_module
|
||||||
import ctypes
|
import ctypes
|
||||||
|
from io import BytesIO
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -105,15 +106,23 @@ from OCP.BRepGProp import BRepGProp
|
||||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||||
from OCP.gp import gp_Pnt
|
from OCP.gp import gp_Pnt
|
||||||
from OCP.GProp import GProp_GProps
|
from OCP.GProp import GProp_GProps
|
||||||
|
from OCP.Standard import Standard_TypeMismatch
|
||||||
from OCP.TopAbs import TopAbs_ShapeEnum
|
from OCP.TopAbs import TopAbs_ShapeEnum
|
||||||
from OCP.TopExp import TopExp_Explorer
|
from OCP.TopExp import TopExp_Explorer
|
||||||
from OCP.TopLoc import TopLoc_Location
|
from OCP.TopLoc import TopLoc_Location
|
||||||
from OCP.TopoDS import TopoDS_Compound
|
from OCP.TopoDS import TopoDS, TopoDS_Compound, TopoDS_Shell
|
||||||
from lib3mf import Lib3MF
|
from lib3mf import Lib3MF
|
||||||
|
|
||||||
from build123d.build_enums import MeshType, Unit
|
from build123d.build_enums import MeshType, Unit
|
||||||
from build123d.geometry import TOLERANCE, Color
|
from build123d.geometry import TOLERANCE, Color
|
||||||
from build123d.topology import Compound, Shape, Shell, Solid, downcast
|
from build123d.topology import (
|
||||||
|
Compound,
|
||||||
|
Shape,
|
||||||
|
Shell,
|
||||||
|
Solid,
|
||||||
|
downcast,
|
||||||
|
unwrap_topods_compound,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Mesher:
|
class Mesher:
|
||||||
|
|
@ -295,7 +304,7 @@ class Mesher:
|
||||||
ocp_mesh_vertices.append(pnt)
|
ocp_mesh_vertices.append(pnt)
|
||||||
|
|
||||||
# Store the triangles from the triangulated faces
|
# Store the triangles from the triangulated faces
|
||||||
if facet.wrapped is None:
|
if not facet:
|
||||||
continue
|
continue
|
||||||
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
|
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
|
||||||
order = [1, 3, 2] if facet_reversed else [1, 2, 3]
|
order = [1, 3, 2] if facet_reversed else [1, 2, 3]
|
||||||
|
|
@ -328,8 +337,7 @@ class Mesher:
|
||||||
|
|
||||||
# Create vertices array in one shot
|
# Create vertices array in one shot
|
||||||
vertices_3mf = [
|
vertices_3mf = [
|
||||||
Lib3MF.Position((ctypes.c_float * 3)(*v))
|
Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys()
|
||||||
for v in vertex_to_idx.keys()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Pre-allocate triangles array and process in bulk
|
# Pre-allocate triangles array and process in bulk
|
||||||
|
|
@ -346,7 +354,9 @@ class Mesher:
|
||||||
|
|
||||||
# Quick degenerate check without set creation
|
# Quick degenerate check without set creation
|
||||||
if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a:
|
if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a:
|
||||||
triangles_3mf.append(Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c)))
|
triangles_3mf.append(
|
||||||
|
Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c))
|
||||||
|
)
|
||||||
|
|
||||||
return (vertices_3mf, triangles_3mf)
|
return (vertices_3mf, triangles_3mf)
|
||||||
|
|
||||||
|
|
@ -464,7 +474,9 @@ class Mesher:
|
||||||
# Convert to a list of gp_Pnt
|
# Convert to a list of gp_Pnt
|
||||||
ocp_vertices = [gp_pnts[tri_indices[i]] for i in range(3)]
|
ocp_vertices = [gp_pnts[tri_indices[i]] for i in range(3)]
|
||||||
# Create the triangular face using the polygon
|
# Create the triangular face using the polygon
|
||||||
polygon_builder = BRepBuilderAPI_MakePolygon(*ocp_vertices, Close=True)
|
polygon_builder = BRepBuilderAPI_MakePolygon(
|
||||||
|
ocp_vertices[0], ocp_vertices[1], ocp_vertices[2], Close=True
|
||||||
|
)
|
||||||
face_builder = BRepBuilderAPI_MakeFace(polygon_builder.Wire())
|
face_builder = BRepBuilderAPI_MakeFace(polygon_builder.Wire())
|
||||||
facet = face_builder.Face()
|
facet = face_builder.Face()
|
||||||
facet_properties = GProp_GProps()
|
facet_properties = GProp_GProps()
|
||||||
|
|
@ -477,19 +489,27 @@ class Mesher:
|
||||||
occ_sewed_shape = downcast(shell_builder.SewedShape())
|
occ_sewed_shape = downcast(shell_builder.SewedShape())
|
||||||
|
|
||||||
if isinstance(occ_sewed_shape, TopoDS_Compound):
|
if isinstance(occ_sewed_shape, TopoDS_Compound):
|
||||||
occ_shells = []
|
bd_shells = []
|
||||||
explorer = TopExp_Explorer(occ_sewed_shape, TopAbs_ShapeEnum.TopAbs_SHELL)
|
explorer = TopExp_Explorer(occ_sewed_shape, TopAbs_ShapeEnum.TopAbs_SHELL)
|
||||||
while explorer.More():
|
while explorer.More():
|
||||||
occ_shells.append(downcast(explorer.Current()))
|
# occ_shells.append(downcast(explorer.Current()))
|
||||||
|
bd_shells.append(Shell(TopoDS.Shell_s(explorer.Current())))
|
||||||
explorer.Next()
|
explorer.Next()
|
||||||
else:
|
else:
|
||||||
occ_shells = [occ_sewed_shape]
|
assert isinstance(occ_sewed_shape, TopoDS_Shell)
|
||||||
|
bd_shells = [Shell(occ_sewed_shape)]
|
||||||
|
|
||||||
# Create a solid if manifold
|
outer_shell = max(bd_shells, key=lambda s: math.prod(s.bounding_box().size))
|
||||||
shape_obj = Shell(occ_sewed_shape)
|
inner_shells = [s for s in bd_shells if s is not outer_shell]
|
||||||
if shape_obj.is_manifold:
|
|
||||||
solid_builder = BRepBuilderAPI_MakeSolid(*occ_shells)
|
# The the shell isn't water tight just return it else create a solid
|
||||||
shape_obj = Solid(solid_builder.Solid())
|
if not outer_shell.is_manifold:
|
||||||
|
return outer_shell
|
||||||
|
|
||||||
|
solid_builder = BRepBuilderAPI_MakeSolid(outer_shell.wrapped)
|
||||||
|
for inner_shell in inner_shells:
|
||||||
|
solid_builder.Add(inner_shell.wrapped)
|
||||||
|
shape_obj = Solid(solid_builder.Solid())
|
||||||
|
|
||||||
return shape_obj
|
return shape_obj
|
||||||
|
|
||||||
|
|
@ -551,3 +571,8 @@ class Mesher:
|
||||||
raise ValueError(f"Unknown file format {output_file_extension}")
|
raise ValueError(f"Unknown file format {output_file_extension}")
|
||||||
writer = self.model.QueryWriter(output_file_extension[1:])
|
writer = self.model.QueryWriter(output_file_extension[1:])
|
||||||
writer.WriteToFile(file_name)
|
writer.WriteToFile(file_name)
|
||||||
|
|
||||||
|
def write_stream(self, stream: BytesIO, file_type: str):
|
||||||
|
writer = self.model.QueryWriter(file_type)
|
||||||
|
result = bytes(writer.WriteToBuffer())
|
||||||
|
stream.write(result)
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ from build123d.build_enums import (
|
||||||
)
|
)
|
||||||
from build123d.build_line import BuildLine
|
from build123d.build_line import BuildLine
|
||||||
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
|
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
|
||||||
from build123d.topology import Edge, Face, Wire, Curve
|
from build123d.topology import Curve, Edge, Face, Vertex, Wire
|
||||||
from build123d.topology.shape_core import ShapeList
|
from build123d.topology.shape_core import ShapeList
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -787,20 +787,22 @@ class Helix(BaseEdgeObject):
|
||||||
|
|
||||||
class FilletPolyline(BaseLineObject):
|
class FilletPolyline(BaseLineObject):
|
||||||
"""Line Object: Fillet Polyline
|
"""Line Object: Fillet Polyline
|
||||||
|
|
||||||
Create a sequence of straight lines defined by successive points that are filleted
|
Create a sequence of straight lines defined by successive points that are filleted
|
||||||
to a given radius.
|
to a given radius.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
|
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.
|
||||||
|
A radius of 0 will create a sharp corner (vertex without fillet).
|
||||||
|
|
||||||
close (bool, optional): close end points with extra Edge and corner fillets.
|
close (bool, optional): close end points with extra Edge and corner fillets.
|
||||||
Defaults to False
|
Defaults to False
|
||||||
|
|
||||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: Two or more points not provided
|
ValueError: Two or more points not provided
|
||||||
ValueError: radius must be positive
|
ValueError: radius must be non-negative
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_applies_to = [BuildLine._tag]
|
_applies_to = [BuildLine._tag]
|
||||||
|
|
@ -808,34 +810,49 @@ class FilletPolyline(BaseLineObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*pts: VectorLike | Iterable[VectorLike],
|
*pts: VectorLike | Iterable[VectorLike],
|
||||||
radius: float,
|
radius: float | Iterable[float],
|
||||||
close: bool = False,
|
close: bool = False,
|
||||||
mode: Mode = Mode.ADD,
|
mode: Mode = Mode.ADD,
|
||||||
):
|
):
|
||||||
|
|
||||||
context: BuildLine | None = BuildLine._get_context(self)
|
context: BuildLine | None = BuildLine._get_context(self)
|
||||||
validate_inputs(context, self)
|
validate_inputs(context, self)
|
||||||
|
|
||||||
points = flatten_sequence(*pts)
|
points = flatten_sequence(*pts)
|
||||||
|
|
||||||
if len(points) < 2:
|
if len(points) < 2:
|
||||||
raise ValueError("FilletPolyline requires two or more pts")
|
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 non-negative")
|
||||||
|
|
||||||
lines_pts = WorkplaneList.localize(*points)
|
lines_pts = WorkplaneList.localize(*points)
|
||||||
|
|
||||||
# Create the polyline
|
# Create the polyline
|
||||||
|
|
||||||
new_edges = [
|
new_edges = [
|
||||||
Edge.make_line(lines_pts[i], lines_pts[i + 1])
|
Edge.make_line(lines_pts[i], lines_pts[i + 1])
|
||||||
for i in range(len(lines_pts) - 1)
|
for i in range(len(lines_pts) - 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5:
|
if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5:
|
||||||
new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0))
|
new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0))
|
||||||
|
|
||||||
wire_of_lines = Wire(new_edges)
|
wire_of_lines = Wire(new_edges)
|
||||||
|
|
||||||
# Create a list of vertices from wire_of_lines in the same order as
|
# Create a list of vertices from wire_of_lines in the same order as
|
||||||
# the original points so the resulting fillet edges are ordered
|
# the original points so the resulting fillet edges are ordered
|
||||||
ordered_vertices = []
|
ordered_vertices: list[Vertex] = []
|
||||||
|
|
||||||
for pnts in lines_pts:
|
for pnts in lines_pts:
|
||||||
distance = {
|
distance = {
|
||||||
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
|
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
|
||||||
|
|
@ -843,40 +860,93 @@ class FilletPolyline(BaseLineObject):
|
||||||
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
|
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
|
||||||
|
|
||||||
# Fillet the corners
|
# Fillet the corners
|
||||||
|
|
||||||
# Create a map of vertices to edges containing that vertex
|
# Create a map of vertices to edges containing that vertex
|
||||||
vertex_to_edges = {
|
vertex_to_edges = {
|
||||||
v: [e for e in wire_of_lines.edges() if v in e.vertices()]
|
v: [e for e in wire_of_lines.edges() if v in e.vertices()]
|
||||||
for v in ordered_vertices
|
for v in ordered_vertices
|
||||||
}
|
}
|
||||||
|
|
||||||
# For each corner vertex create a new fillet Edge
|
# For each corner vertex create a new fillet Edge (or keep as vertex if radius is 0)
|
||||||
fillets = []
|
fillets: list[None | Edge] = []
|
||||||
for vertex, edges in vertex_to_edges.items():
|
|
||||||
|
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
|
||||||
if len(edges) != 2:
|
if len(edges) != 2:
|
||||||
continue
|
continue
|
||||||
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
|
current_radius = radius_list[i - int(not close)]
|
||||||
third_edge = Edge.make_line(*[v for v in other_vertices])
|
|
||||||
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex])
|
if current_radius == 0:
|
||||||
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
# For 0 radius, store the vertex as a marker for a sharp corner
|
||||||
|
fillets.append(None)
|
||||||
|
|
||||||
|
else:
|
||||||
|
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(
|
||||||
|
current_radius, [vertex]
|
||||||
|
)
|
||||||
|
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
||||||
|
|
||||||
# Create the Edges that join the fillets
|
# Create the Edges that join the fillets
|
||||||
if close:
|
if close:
|
||||||
interior_edges = [
|
interior_edges = []
|
||||||
Edge.make_line(fillets[i - 1] @ 1, fillets[i] @ 0)
|
|
||||||
for i in range(len(fillets))
|
|
||||||
]
|
|
||||||
end_edges = []
|
|
||||||
else:
|
|
||||||
interior_edges = [
|
|
||||||
Edge.make_line(fillets[i] @ 1, f @ 0) for i, f in enumerate(fillets[1:])
|
|
||||||
]
|
|
||||||
end_edges = [
|
|
||||||
Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0),
|
|
||||||
Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1),
|
|
||||||
]
|
|
||||||
|
|
||||||
new_wire = Wire(end_edges + interior_edges + fillets)
|
for i in range(len(fillets)):
|
||||||
|
prev_fillet = fillets[i - 1]
|
||||||
|
curr_fillet = fillets[i]
|
||||||
|
prev_idx = i - 1
|
||||||
|
curr_idx = i
|
||||||
|
# Determine start and end points
|
||||||
|
if prev_fillet is None:
|
||||||
|
start_pt: Vertex | Vector = ordered_vertices[prev_idx]
|
||||||
|
else:
|
||||||
|
start_pt = prev_fillet @ 1
|
||||||
|
|
||||||
|
if curr_fillet is None:
|
||||||
|
end_pt: Vertex | Vector = ordered_vertices[curr_idx]
|
||||||
|
else:
|
||||||
|
end_pt = curr_fillet @ 0
|
||||||
|
interior_edges.append(Edge.make_line(start_pt, end_pt))
|
||||||
|
|
||||||
|
end_edges = []
|
||||||
|
|
||||||
|
else:
|
||||||
|
interior_edges = []
|
||||||
|
for i in range(len(fillets) - 1):
|
||||||
|
next_fillet = fillets[i + 1]
|
||||||
|
curr_fillet = fillets[i]
|
||||||
|
curr_idx = i
|
||||||
|
next_idx = i + 1
|
||||||
|
# Determine start and end points
|
||||||
|
if curr_fillet is None:
|
||||||
|
start_pt = ordered_vertices[
|
||||||
|
curr_idx + 1
|
||||||
|
] # +1 because first vertex has no fillet
|
||||||
|
else:
|
||||||
|
start_pt = curr_fillet @ 1
|
||||||
|
|
||||||
|
if next_fillet is None:
|
||||||
|
end_pt = ordered_vertices[next_idx + 1]
|
||||||
|
else:
|
||||||
|
end_pt = next_fillet @ 0
|
||||||
|
interior_edges.append(Edge.make_line(start_pt, end_pt))
|
||||||
|
|
||||||
|
# Handle end edges
|
||||||
|
if fillets[0] is None:
|
||||||
|
start_edge = Edge.make_line(wire_of_lines @ 0, ordered_vertices[1])
|
||||||
|
else:
|
||||||
|
start_edge = Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0)
|
||||||
|
|
||||||
|
if fillets[-1] is None:
|
||||||
|
end_edge = Edge.make_line(ordered_vertices[-2], wire_of_lines @ 1)
|
||||||
|
else:
|
||||||
|
end_edge = Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1)
|
||||||
|
end_edges = [start_edge, end_edge]
|
||||||
|
|
||||||
|
# Filter out None values from fillets (these are 0-radius corners)
|
||||||
|
actual_fillets = [f for f in fillets if f is not None]
|
||||||
|
new_wire = Wire(end_edges + interior_edges + actual_fillets)
|
||||||
|
|
||||||
super().__init__(new_wire, mode=mode)
|
super().__init__(new_wire, mode=mode)
|
||||||
|
|
||||||
|
|
@ -1597,6 +1667,7 @@ class ArcArcTangentLine(BaseEdgeObject):
|
||||||
Defaults to Keep.INSIDE
|
Defaults to Keep.INSIDE
|
||||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||||
"""
|
"""
|
||||||
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
|
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@ def chamfer(
|
||||||
|
|
||||||
if target._dim == 1:
|
if target._dim == 1:
|
||||||
if isinstance(target, BaseLineObject):
|
if isinstance(target, BaseLineObject):
|
||||||
if target.wrapped is None:
|
if not target:
|
||||||
target = Wire([]) # empty wire
|
target = Wire([]) # empty wire
|
||||||
else:
|
else:
|
||||||
target = Wire(target.wrapped)
|
target = Wire(target.wrapped)
|
||||||
|
|
@ -465,7 +465,7 @@ def fillet(
|
||||||
|
|
||||||
if target._dim == 1:
|
if target._dim == 1:
|
||||||
if isinstance(target, BaseLineObject):
|
if isinstance(target, BaseLineObject):
|
||||||
if target.wrapped is None:
|
if not target:
|
||||||
target = Wire([]) # empty wire
|
target = Wire([]) # empty wire
|
||||||
else:
|
else:
|
||||||
target = Wire(target.wrapped)
|
target = Wire(target.wrapped)
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,8 @@ def extrude(
|
||||||
|
|
||||||
new_solids.append(
|
new_solids.append(
|
||||||
Solid.extrude_until(
|
Solid.extrude_until(
|
||||||
section=face,
|
face,
|
||||||
target_object=target_object,
|
target=target_object,
|
||||||
direction=plane.z_dir * direction,
|
direction=plane.z_dir * direction,
|
||||||
until=until,
|
until=until,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ from build123d.topology import (
|
||||||
Wire,
|
Wire,
|
||||||
Sketch,
|
Sketch,
|
||||||
topo_explore_connected_edges,
|
topo_explore_connected_edges,
|
||||||
topo_explore_common_vertex,
|
|
||||||
)
|
)
|
||||||
from build123d.geometry import Plane, Vector, TOLERANCE
|
from build123d.geometry import Plane, Vector, TOLERANCE
|
||||||
from build123d.build_common import flatten_sequence, validate_inputs
|
from build123d.build_common import flatten_sequence, validate_inputs
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,13 @@ import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from itertools import combinations
|
|
||||||
from typing import Type, Union
|
|
||||||
|
|
||||||
from collections.abc import Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
|
from itertools import combinations
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
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 (
|
from OCP.Font import (
|
||||||
Font_FA_Bold,
|
Font_FA_Bold,
|
||||||
Font_FA_BoldItalic,
|
Font_FA_BoldItalic,
|
||||||
|
|
@ -107,7 +107,6 @@ from build123d.geometry import (
|
||||||
VectorLike,
|
VectorLike,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from .one_d import Edge, Wire, Mixin1D
|
from .one_d import Edge, Wire, Mixin1D
|
||||||
from .shape_core import (
|
from .shape_core import (
|
||||||
|
|
@ -130,7 +129,7 @@ from .utils import (
|
||||||
from .zero_d import Vertex
|
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
|
"""A Compound in build123d is a topological entity representing a collection of
|
||||||
geometric shapes grouped together within a single structure. It serves as a
|
geometric shapes grouped together within a single structure. It serves as a
|
||||||
container for organizing diverse shapes like edges, faces, or solids. This
|
container for organizing diverse shapes like edges, faces, or solids. This
|
||||||
|
|
@ -142,7 +141,6 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
|
|
||||||
order = 4.0
|
order = 4.0
|
||||||
|
|
||||||
project_to_viewport = Mixin1D.project_to_viewport
|
|
||||||
# ---- Constructor ----
|
# ---- Constructor ----
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -166,7 +164,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
parent (Compound, optional): assembly parent. Defaults to None.
|
parent (Compound, optional): assembly parent. Defaults to None.
|
||||||
children (Sequence[Shape], optional): assembly children. Defaults to None.
|
children (Sequence[Shape], optional): assembly children. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
topods_compound: TopoDS_Compound | None
|
||||||
if isinstance(obj, Iterable):
|
if isinstance(obj, Iterable):
|
||||||
topods_compound = _make_topods_compound_from_shapes(
|
topods_compound = _make_topods_compound_from_shapes(
|
||||||
[s.wrapped for s in obj]
|
[s.wrapped for s in obj]
|
||||||
|
|
@ -378,8 +376,14 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
)
|
)
|
||||||
|
|
||||||
text_flat = Compound(
|
text_flat = Compound(
|
||||||
builder.Perform(
|
TopoDS.Compound_s(
|
||||||
font_i, NCollection_Utf8String(txt), gp_Ax3(), horiz_align, vert_align
|
builder.Perform(
|
||||||
|
font_i,
|
||||||
|
NCollection_Utf8String(txt),
|
||||||
|
gp_Ax3(),
|
||||||
|
horiz_align,
|
||||||
|
vert_align,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -455,7 +459,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
will be a Wire, otherwise a Shape.
|
will be a Wire, otherwise a Shape.
|
||||||
"""
|
"""
|
||||||
if self._dim == 1:
|
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
|
sum1d: Edge | Wire | ShapeList[Edge] = curve + other
|
||||||
if isinstance(sum1d, ShapeList):
|
if isinstance(sum1d, ShapeList):
|
||||||
result1d: Curve | Wire = Curve(sum1d)
|
result1d: Curve | Wire = Curve(sum1d)
|
||||||
|
|
@ -506,6 +510,8 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
def __and__(self, other: Shape | Iterable[Shape]) -> Compound:
|
def __and__(self, other: Shape | Iterable[Shape]) -> Compound:
|
||||||
"""Intersect other to self `&` operator"""
|
"""Intersect other to self `&` operator"""
|
||||||
intersection = Shape.__and__(self, other)
|
intersection = Shape.__and__(self, other)
|
||||||
|
if intersection is None:
|
||||||
|
return Compound()
|
||||||
intersection = Compound(
|
intersection = Compound(
|
||||||
intersection if isinstance(intersection, list) else [intersection]
|
intersection if isinstance(intersection, list) else [intersection]
|
||||||
)
|
)
|
||||||
|
|
@ -517,7 +523,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
Check if empty.
|
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]:
|
def __iter__(self) -> Iterator[Shape]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -534,7 +540,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Return the number of subshapes"""
|
"""Return the number of subshapes"""
|
||||||
count = 0
|
count = 0
|
||||||
if self.wrapped is not None:
|
if self._wrapped is not None:
|
||||||
for _ in self:
|
for _ in self:
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
@ -593,7 +599,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
"""Return the Compound"""
|
"""Return the Compound"""
|
||||||
shape_list = self.compounds()
|
shape_list = self.compounds()
|
||||||
entity_count = len(shape_list)
|
entity_count = len(shape_list)
|
||||||
if entity_count != 1:
|
if entity_count > 1:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Found {entity_count} compounds, returning first",
|
f"Found {entity_count} compounds, returning first",
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
|
|
@ -602,7 +608,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
|
|
||||||
def compounds(self) -> ShapeList[Compound]:
|
def compounds(self) -> ShapeList[Compound]:
|
||||||
"""compounds - all the compounds in this Shape"""
|
"""compounds - all the compounds in this Shape"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return ShapeList()
|
return ShapeList()
|
||||||
if isinstance(self.wrapped, TopoDS_Compound):
|
if isinstance(self.wrapped, TopoDS_Compound):
|
||||||
# pylint: disable=not-an-iterable
|
# pylint: disable=not-an-iterable
|
||||||
|
|
@ -651,11 +657,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
children[child_index_pair[1]]
|
children[child_index_pair[1]]
|
||||||
)
|
)
|
||||||
if obj_intersection is not None:
|
if obj_intersection is not None:
|
||||||
common_volume = (
|
common_volume = sum(s.volume for s in obj_intersection.solids())
|
||||||
0.0
|
|
||||||
if isinstance(obj_intersection, list)
|
|
||||||
else obj_intersection.volume
|
|
||||||
)
|
|
||||||
if common_volume > tolerance:
|
if common_volume > tolerance:
|
||||||
return (
|
return (
|
||||||
True,
|
True,
|
||||||
|
|
@ -706,11 +708,182 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
while iterator.More():
|
while iterator.More():
|
||||||
child = iterator.Value()
|
child = iterator.Value()
|
||||||
if child.ShapeType() == type_map[obj_type]:
|
if child.ShapeType() == type_map[obj_type]:
|
||||||
results.append(obj_type(downcast(child)))
|
results.append(obj_type(downcast(child))) # type: ignore
|
||||||
iterator.Next()
|
iterator.Next()
|
||||||
|
|
||||||
return results
|
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 | BRepAlgoAPI_Common = (
|
||||||
|
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 project_to_viewport(
|
||||||
|
self,
|
||||||
|
viewport_origin: VectorLike,
|
||||||
|
viewport_up: VectorLike = (0, 0, 1),
|
||||||
|
look_at: VectorLike | None = None,
|
||||||
|
focus: float | None = None,
|
||||||
|
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
|
||||||
|
"""project_to_viewport
|
||||||
|
|
||||||
|
Project a shape onto a viewport returning visible and hidden Edges.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
viewport_origin (VectorLike): location of viewport
|
||||||
|
viewport_up (VectorLike, optional): direction of the viewport y axis.
|
||||||
|
Defaults to (0, 0, 1).
|
||||||
|
look_at (VectorLike, optional): point to look at.
|
||||||
|
Defaults to None (center of shape).
|
||||||
|
focus (float, optional): the focal length for perspective projection
|
||||||
|
Defaults to None (orthographic projection)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
|
||||||
|
"""
|
||||||
|
return Mixin1D.project_to_viewport(
|
||||||
|
self, viewport_origin, viewport_up, look_at, focus
|
||||||
|
)
|
||||||
|
|
||||||
def unwrap(self, fully: bool = True) -> Self | Shape:
|
def unwrap(self, fully: bool = True) -> Self | Shape:
|
||||||
"""Strip unnecessary Compound wrappers
|
"""Strip unnecessary Compound wrappers
|
||||||
|
|
||||||
|
|
@ -764,8 +937,8 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
|
||||||
parent.wrapped = _make_topods_compound_from_shapes(
|
parent.wrapped = _make_topods_compound_from_shapes(
|
||||||
[c.wrapped for c in parent.children]
|
[c.wrapped for c in parent.children]
|
||||||
)
|
)
|
||||||
else:
|
# else:
|
||||||
parent.wrapped = None
|
# parent.wrapped = None
|
||||||
|
|
||||||
def _post_detach_children(self, children):
|
def _post_detach_children(self, children):
|
||||||
"""Method call before detaching `children`."""
|
"""Method call before detaching `children`."""
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
|
||||||
- Edge -> (QualifiedCurve, h2d, first, last, True)
|
- Edge -> (QualifiedCurve, h2d, first, last, True)
|
||||||
- Vector -> (CartesianPoint, None, None, None, False)
|
- 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")
|
raise TypeError("Can't create a qualified curve from empty edge")
|
||||||
|
|
||||||
if isinstance(obj.wrapped, TopoDS_Edge):
|
if isinstance(obj.wrapped, TopoDS_Edge):
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,11 @@ license:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import numpy as np
|
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Sequence
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
|
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
|
from typing import cast as tcast
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -90,8 +89,12 @@ from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||||
from OCP.BRepProj import BRepProj_Projection
|
from OCP.BRepProj import BRepProj_Projection
|
||||||
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
||||||
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse
|
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse
|
||||||
from OCP.GccEnt import GccEnt_unqualified, GccEnt_Position
|
from OCP.GCPnts import (
|
||||||
from OCP.GCPnts import GCPnts_AbscissaPoint
|
GCPnts_AbscissaPoint,
|
||||||
|
GCPnts_QuasiUniformDeflection,
|
||||||
|
GCPnts_UniformDeflection,
|
||||||
|
)
|
||||||
|
from OCP.GProp import GProp_GProps
|
||||||
from OCP.Geom import (
|
from OCP.Geom import (
|
||||||
Geom_BezierCurve,
|
Geom_BezierCurve,
|
||||||
Geom_BSplineCurve,
|
Geom_BSplineCurve,
|
||||||
|
|
@ -117,6 +120,9 @@ from OCP.GeomAbs import (
|
||||||
GeomAbs_C0,
|
GeomAbs_C0,
|
||||||
GeomAbs_C1,
|
GeomAbs_C1,
|
||||||
GeomAbs_C2,
|
GeomAbs_C2,
|
||||||
|
GeomAbs_C3,
|
||||||
|
GeomAbs_CN,
|
||||||
|
GeomAbs_C1,
|
||||||
GeomAbs_G1,
|
GeomAbs_G1,
|
||||||
GeomAbs_G2,
|
GeomAbs_G2,
|
||||||
GeomAbs_JoinType,
|
GeomAbs_JoinType,
|
||||||
|
|
@ -217,6 +223,7 @@ from build123d.geometry import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .shape_core import (
|
from .shape_core import (
|
||||||
|
TOPODS,
|
||||||
Shape,
|
Shape,
|
||||||
ShapeList,
|
ShapeList,
|
||||||
SkipClean,
|
SkipClean,
|
||||||
|
|
@ -226,11 +233,11 @@ from .shape_core import (
|
||||||
shapetype,
|
shapetype,
|
||||||
topods_dim,
|
topods_dim,
|
||||||
unwrap_topods_compound,
|
unwrap_topods_compound,
|
||||||
|
_topods_bool_op,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
_make_topods_face_from_wires,
|
_make_topods_face_from_wires,
|
||||||
_topods_bool_op,
|
|
||||||
isclose_b,
|
isclose_b,
|
||||||
)
|
)
|
||||||
from .zero_d import Vertex, topo_explore_common_vertex
|
from .zero_d import Vertex, topo_explore_common_vertex
|
||||||
|
|
@ -250,7 +257,7 @@ if TYPE_CHECKING: # pragma: no cover
|
||||||
from .two_d import Face, Shell # pylint: disable=R0801
|
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"""
|
"""Methods to add to the Edge and Wire classes"""
|
||||||
|
|
||||||
# ---- Properties ----
|
# ---- Properties ----
|
||||||
|
|
@ -263,14 +270,14 @@ class Mixin1D(Shape):
|
||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
"""Are the start and end points equal?"""
|
"""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")
|
raise ValueError("Can't determine if empty Edge or Wire is closed")
|
||||||
return BRep_Tool.IsClosed_s(self.wrapped)
|
return BRep_Tool.IsClosed_s(self.wrapped)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_forward(self) -> bool:
|
def is_forward(self) -> bool:
|
||||||
"""Does the Edge/Wire loop forward or reverse"""
|
"""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")
|
raise ValueError("Can't determine direction of empty Edge or Wire")
|
||||||
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
|
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
|
||||||
|
|
||||||
|
|
@ -305,7 +312,9 @@ class Mixin1D(Shape):
|
||||||
@property
|
@property
|
||||||
def length(self) -> float:
|
def length(self) -> float:
|
||||||
"""Edge or Wire length"""
|
"""Edge or Wire length"""
|
||||||
return GCPnts_AbscissaPoint.Length_s(self.geom_adaptor())
|
props = GProp_GProps()
|
||||||
|
BRepGProp.LinearProperties_s(self.wrapped, props)
|
||||||
|
return props.Mass()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def radius(self) -> float:
|
def radius(self) -> float:
|
||||||
|
|
@ -388,8 +397,7 @@ class Mixin1D(Shape):
|
||||||
shape
|
shape
|
||||||
# for o in (other if isinstance(other, (list, tuple)) else [other])
|
# for o in (other if isinstance(other, (list, tuple)) else [other])
|
||||||
for o in ([other] if isinstance(other, Shape) 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 if o else None)
|
||||||
for shape in get_top_level_topods_shapes(o.wrapped)
|
|
||||||
]
|
]
|
||||||
# If there is nothing to add return the original object
|
# If there is nothing to add return the original object
|
||||||
if not topods_summands:
|
if not topods_summands:
|
||||||
|
|
@ -404,7 +412,7 @@ class Mixin1D(Shape):
|
||||||
)
|
)
|
||||||
summand_edges = [e for summand in summands for e in summand.edges()]
|
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:
|
if len(summands) == 1:
|
||||||
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
|
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
|
||||||
else:
|
else:
|
||||||
|
|
@ -452,7 +460,7 @@ class Mixin1D(Shape):
|
||||||
Returns:
|
Returns:
|
||||||
Vector: center
|
Vector: center
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't find center of empty edge/wire")
|
raise ValueError("Can't find center of empty edge/wire")
|
||||||
|
|
||||||
if center_of == CenterOf.GEOMETRY:
|
if center_of == CenterOf.GEOMETRY:
|
||||||
|
|
@ -571,14 +579,14 @@ class Mixin1D(Shape):
|
||||||
- On straight segments, κ = 0 so no teeth are drawn.
|
- On straight segments, κ = 0 so no teeth are drawn.
|
||||||
- At inflection points κ→0 and the tooth flips direction.
|
- At inflection points κ→0 and the tooth flips direction.
|
||||||
- At C0 corners the tangent is discontinuous; nearby teeth may jump.
|
- At C0 corners the tangent is discontinuous; nearby teeth may jump.
|
||||||
C1 yields continuous direction; C2 yields continuous magnitude as well.
|
C1 yields continuous direction; C2 yields continuous magnitude as well.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0)
|
>>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0)
|
||||||
>>> show(my_wire, Curve(comb))
|
>>> 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")
|
raise ValueError("Can't create curvature_comb for empty curve")
|
||||||
pln = self.common_plane()
|
pln = self.common_plane()
|
||||||
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
|
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
|
||||||
|
|
@ -676,30 +684,30 @@ class Mixin1D(Shape):
|
||||||
|
|
||||||
return derivative
|
return derivative
|
||||||
|
|
||||||
def edge(self) -> Edge | None:
|
# def edge(self) -> Edge | None:
|
||||||
"""Return the Edge"""
|
# """Return the Edge"""
|
||||||
return Shape.get_single_shape(self, "Edge")
|
# return Shape.get_single_shape(self, "Edge")
|
||||||
|
|
||||||
def edges(self) -> ShapeList[Edge]:
|
# def edges(self) -> ShapeList[Edge]:
|
||||||
"""edges - all the edges in this Shape"""
|
# """edges - all the edges in this Shape"""
|
||||||
if isinstance(self, Wire) and self.wrapped is not None:
|
# if isinstance(self, Wire) and self.wrapped is not None:
|
||||||
# The WireExplorer is a tool to explore the edges of a wire in a connection order.
|
# # The WireExplorer is a tool to explore the edges of a wire in a connection order.
|
||||||
explorer = BRepTools_WireExplorer(self.wrapped)
|
# explorer = BRepTools_WireExplorer(self.wrapped)
|
||||||
|
|
||||||
edge_list: ShapeList[Edge] = ShapeList()
|
# edge_list: ShapeList[Edge] = ShapeList()
|
||||||
while explorer.More():
|
# while explorer.More():
|
||||||
next_edge = Edge(explorer.Current())
|
# next_edge = Edge(explorer.Current())
|
||||||
next_edge.topo_parent = (
|
# next_edge.topo_parent = (
|
||||||
self if self.topo_parent is None else self.topo_parent
|
# self if self.topo_parent is None else self.topo_parent
|
||||||
)
|
# )
|
||||||
edge_list.append(next_edge)
|
# edge_list.append(next_edge)
|
||||||
explorer.Next()
|
# explorer.Next()
|
||||||
return edge_list
|
# return edge_list
|
||||||
|
|
||||||
edge_list = Shape.get_shape_list(self, "Edge")
|
# edge_list = Shape.get_shape_list(self, "Edge")
|
||||||
return edge_list.filter_by(
|
# return edge_list.filter_by(
|
||||||
lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
|
# lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
|
||||||
)
|
# )
|
||||||
|
|
||||||
def end_point(self) -> Vector:
|
def end_point(self) -> Vector:
|
||||||
"""The end point of this edge.
|
"""The end point of this edge.
|
||||||
|
|
@ -729,122 +737,104 @@ class Mixin1D(Shape):
|
||||||
def to_vertex(objs: Iterable) -> ShapeList:
|
def to_vertex(objs: Iterable) -> ShapeList:
|
||||||
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
|
||||||
|
|
||||||
common_set: ShapeList[Vertex | Edge] = ShapeList(self.edges())
|
def bool_op(
|
||||||
target: ShapeList | Shape | Plane
|
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:
|
for other in to_intersect:
|
||||||
# Conform target type
|
# Conform target type
|
||||||
# Vertices need to be Vector for set()
|
|
||||||
match other:
|
match other:
|
||||||
case Axis():
|
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():
|
case Plane():
|
||||||
target = other
|
target = other
|
||||||
case Vector():
|
case Vector():
|
||||||
target = Vertex(other)
|
target = Vertex(other)
|
||||||
case Location():
|
case Location():
|
||||||
target = Vertex(other.position)
|
target = Vertex(other.position)
|
||||||
case Edge():
|
|
||||||
target = ShapeList([other])
|
|
||||||
case Wire():
|
|
||||||
target = ShapeList(other.edges())
|
|
||||||
case _ if issubclass(type(other), Shape):
|
case _ if issubclass(type(other), Shape):
|
||||||
target = other
|
target = other
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
|
||||||
|
|
||||||
# Find common matches
|
# Find common matches
|
||||||
common: list[Vector | Edge] = []
|
common: list[Vertex | Edge | Wire] = []
|
||||||
result: ShapeList | Shape | None
|
result: ShapeList | None
|
||||||
for obj in common_set:
|
for obj in common_set:
|
||||||
match (obj, target):
|
match (obj, target):
|
||||||
case obj, Shape() as target:
|
case (_, Plane()):
|
||||||
# Find Shape with Edge/Wire
|
assert isinstance(other.wrapped, gp_Pln)
|
||||||
if isinstance(target, Vertex):
|
target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
|
||||||
result = Shape.intersect(obj, target)
|
operation1 = BRepAlgoAPI_Section()
|
||||||
else:
|
result = bool_op((obj,), (target,), operation1)
|
||||||
result = target.intersect(obj)
|
operation2 = BRepAlgoAPI_Common()
|
||||||
|
result.extend(bool_op((obj,), (target,), operation2))
|
||||||
|
|
||||||
if result:
|
case (_, Vertex() | Edge() | Wire()):
|
||||||
if not isinstance(result, list):
|
operation1 = BRepAlgoAPI_Section()
|
||||||
result = ShapeList([result])
|
section = bool_op((obj,), (target,), operation1)
|
||||||
common.extend(to_vector(result))
|
result = section
|
||||||
|
if not section:
|
||||||
|
operation2 = BRepAlgoAPI_Common()
|
||||||
|
result.extend(bool_op((obj,), (target,), operation2))
|
||||||
|
|
||||||
case Vertex() as obj, target:
|
case _ if issubclass(type(target), Shape):
|
||||||
if not isinstance(target, ShapeList):
|
result = target.intersect(obj)
|
||||||
target = ShapeList([target])
|
|
||||||
|
|
||||||
for tar in target:
|
if result:
|
||||||
if isinstance(tar, Edge):
|
common.extend(result)
|
||||||
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 common:
|
if common:
|
||||||
common_set = to_vertex(set(common))
|
common_set = ShapeList()
|
||||||
# Remove Vertex intersections coincident to Edge intersections
|
for shape in common:
|
||||||
vts = common_set.vertices()
|
if isinstance(shape, Wire):
|
||||||
eds = common_set.edges()
|
common_set.extend(shape.edges())
|
||||||
if vts and eds:
|
else:
|
||||||
filtered_vts = ShapeList(
|
common_set.append(shape)
|
||||||
[
|
common_set = to_vertex(set(to_vector(common_set)))
|
||||||
v
|
common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
|
||||||
for v in vts
|
|
||||||
if all(v.distance_to(e) > TOLERANCE for e in eds)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
common_set = filtered_vts + eds
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -991,7 +981,7 @@ class Mixin1D(Shape):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't find normal of empty edge/wire")
|
raise ValueError("Can't find normal of empty edge/wire")
|
||||||
|
|
||||||
curve = self.geom_adaptor()
|
curve = self.geom_adaptor()
|
||||||
|
|
@ -1117,16 +1107,16 @@ class Mixin1D(Shape):
|
||||||
The meaning of the returned parameter depends on the type of self:
|
The meaning of the returned parameter depends on the type of self:
|
||||||
|
|
||||||
- **Edge**: Returns the native OCCT curve parameter corresponding to the
|
- **Edge**: Returns the native OCCT curve parameter corresponding to the
|
||||||
given normalized `position` (0.0 → start, 1.0 → end). For closed/periodic
|
given normalized `position` (0.0 → start, 1.0 → end). For closed/periodic
|
||||||
edges, OCCT may return a value **outside** the edge's nominal parameter
|
edges, OCCT may return a value **outside** the edge's nominal parameter
|
||||||
range `[param_min, param_max]` (e.g., by adding/subtracting multiples of
|
range `[param_min, param_max]` (e.g., by adding/subtracting multiples of
|
||||||
the period). If you require a value folded into the edge's range, apply a
|
the period). If you require a value folded into the edge's range, apply a
|
||||||
modulo with the parameter span.
|
modulo with the parameter span.
|
||||||
|
|
||||||
- **Wire**: Returns a *composite* parameter encoding both the edge index
|
- **Wire**: Returns a *composite* parameter encoding both the edge index
|
||||||
and the position within that edge: the **integer part** is the zero-based
|
and the position within that edge: the **integer part** is the zero-based
|
||||||
count of fully traversed edges, and the **fractional part** is the
|
count of fully traversed edges, and the **fractional part** is the
|
||||||
normalized position in `[0.0, 1.0]` along the current edge.
|
normalized position in `[0.0, 1.0]` along the current edge.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position (float): Normalized arc-length position along the shape,
|
position (float): Normalized arc-length position along the shape,
|
||||||
|
|
@ -1195,22 +1185,52 @@ class Mixin1D(Shape):
|
||||||
|
|
||||||
def positions(
|
def positions(
|
||||||
self,
|
self,
|
||||||
distances: Iterable[float],
|
distances: Iterable[float] | None = None,
|
||||||
position_mode: PositionMode = PositionMode.PARAMETER,
|
position_mode: PositionMode = PositionMode.PARAMETER,
|
||||||
|
deflection: float | None = None,
|
||||||
) -> list[Vector]:
|
) -> list[Vector]:
|
||||||
"""Positions along curve
|
"""Positions along curve
|
||||||
|
|
||||||
Generate positions along the underlying curve
|
Generate positions along the underlying curve
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
distances (Iterable[float]): distance or parameter values
|
distances (Iterable[float] | None, optional): distance or parameter values.
|
||||||
position_mode (PositionMode, optional): position calculation mode.
|
Defaults to None.
|
||||||
Defaults to PositionMode.PARAMETER.
|
position_mode (PositionMode, optional): position calculation mode only applies
|
||||||
|
when using distances. Defaults to PositionMode.PARAMETER.
|
||||||
|
deflection (float | None, optional): maximum deflection between the curve and
|
||||||
|
the polygon that results from the computed points. Defaults to None.
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[Vector]: positions along curve
|
list[Vector]: positions along curve
|
||||||
"""
|
"""
|
||||||
return [self.position_at(d, position_mode) for d in distances]
|
if deflection is not None:
|
||||||
|
curve: BRepAdaptor_Curve | BRepAdaptor_CompCurve = self.geom_adaptor()
|
||||||
|
# GCPnts_UniformDeflection provides the best results but is limited
|
||||||
|
if curve.Continuity() in (GeomAbs_C2, GeomAbs_C3, GeomAbs_CN):
|
||||||
|
discretizer: (
|
||||||
|
GCPnts_UniformDeflection | GCPnts_QuasiUniformDeflection
|
||||||
|
) = GCPnts_UniformDeflection()
|
||||||
|
else:
|
||||||
|
discretizer = GCPnts_QuasiUniformDeflection()
|
||||||
|
|
||||||
|
discretizer.Initialize(
|
||||||
|
curve,
|
||||||
|
deflection,
|
||||||
|
curve.FirstParameter(),
|
||||||
|
curve.LastParameter(),
|
||||||
|
)
|
||||||
|
if not discretizer.IsDone() or discretizer.NbPoints() == 0:
|
||||||
|
raise RuntimeError("Deflection calculation failed")
|
||||||
|
return [
|
||||||
|
Vector(curve.Value(discretizer.Parameter(i + 1)))
|
||||||
|
for i in range(discretizer.NbPoints())
|
||||||
|
]
|
||||||
|
elif distances is not None:
|
||||||
|
return [self.position_at(d, position_mode) for d in distances]
|
||||||
|
else:
|
||||||
|
raise ValueError("Either distances or deflection must be provided")
|
||||||
|
|
||||||
def project(
|
def project(
|
||||||
self, face: Face, direction: VectorLike, closest: bool = True
|
self, face: Face, direction: VectorLike, closest: bool = True
|
||||||
|
|
@ -1225,7 +1245,7 @@ class Mixin1D(Shape):
|
||||||
Returns:
|
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")
|
raise ValueError("Can't project an empty Edge or Wire onto empty Face")
|
||||||
|
|
||||||
bldr = BRepProj_Projection(
|
bldr = BRepProj_Projection(
|
||||||
|
|
@ -1297,7 +1317,7 @@ class Mixin1D(Shape):
|
||||||
|
|
||||||
return edges
|
return edges
|
||||||
|
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't project empty edge/wire")
|
raise ValueError("Can't project empty edge/wire")
|
||||||
|
|
||||||
# Setup the projector
|
# Setup the projector
|
||||||
|
|
@ -1357,144 +1377,6 @@ class Mixin1D(Shape):
|
||||||
|
|
||||||
return (visible_edges, hidden_edges)
|
return (visible_edges, hidden_edges)
|
||||||
|
|
||||||
@overload
|
|
||||||
def split(
|
|
||||||
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
|
|
||||||
) -> Self | list[Self] | None:
|
|
||||||
"""split and keep inside or outside"""
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
|
|
||||||
"""split and return the unordered pieces"""
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
|
|
||||||
Self | list[Self] | None,
|
|
||||||
Self | list[Self] | None,
|
|
||||||
]:
|
|
||||||
"""split and keep inside and outside"""
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
|
|
||||||
"""split and keep inside (default)"""
|
|
||||||
|
|
||||||
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
|
||||||
"""split
|
|
||||||
|
|
||||||
Split this shape by the provided plane or face.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
surface (Plane | Face): surface to segment shape
|
|
||||||
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Shape: result of split
|
|
||||||
Returns:
|
|
||||||
Self | list[Self] | None,
|
|
||||||
Tuple[Self | list[Self] | None]: The result of the split operation.
|
|
||||||
|
|
||||||
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
|
|
||||||
if no top is found.
|
|
||||||
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
|
|
||||||
if no bottom is found.
|
|
||||||
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
|
|
||||||
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
|
|
||||||
"""
|
|
||||||
if self.wrapped is None or tool.wrapped is None:
|
|
||||||
raise ValueError("Can't split an empty edge/wire/tool")
|
|
||||||
|
|
||||||
shape_list = TopTools_ListOfShape()
|
|
||||||
shape_list.Append(self.wrapped)
|
|
||||||
|
|
||||||
# Define the splitting tool
|
|
||||||
trim_tool = (
|
|
||||||
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
|
|
||||||
if isinstance(tool, Plane)
|
|
||||||
else tool.wrapped
|
|
||||||
)
|
|
||||||
tool_list = TopTools_ListOfShape()
|
|
||||||
tool_list.Append(trim_tool)
|
|
||||||
|
|
||||||
# Create the splitter algorithm
|
|
||||||
splitter = BRepAlgoAPI_Splitter()
|
|
||||||
|
|
||||||
# Set the shape to be split and the splitting tool (plane face)
|
|
||||||
splitter.SetArguments(shape_list)
|
|
||||||
splitter.SetTools(tool_list)
|
|
||||||
|
|
||||||
# Perform the splitting operation
|
|
||||||
splitter.Build()
|
|
||||||
|
|
||||||
split_result = downcast(splitter.Shape())
|
|
||||||
# Remove unnecessary TopoDS_Compound around single shape
|
|
||||||
if isinstance(split_result, TopoDS_Compound):
|
|
||||||
split_result = unwrap_topods_compound(split_result, True)
|
|
||||||
|
|
||||||
# For speed the user may just want all the objects which they
|
|
||||||
# can sort more efficiently then the generic algorithm below
|
|
||||||
if keep == Keep.ALL:
|
|
||||||
return ShapeList(
|
|
||||||
self.__class__.cast(part)
|
|
||||||
for part in get_top_level_topods_shapes(split_result)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(tool, Plane):
|
|
||||||
# Get a TopoDS_Face to work with from the tool
|
|
||||||
if isinstance(trim_tool, TopoDS_Shell):
|
|
||||||
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
|
|
||||||
tool_face = TopoDS.Face_s(face_explorer.Current())
|
|
||||||
else:
|
|
||||||
tool_face = trim_tool
|
|
||||||
|
|
||||||
# Create a reference point off the +ve side of the tool
|
|
||||||
surface_gppnt = gp_Pnt()
|
|
||||||
surface_normal = gp_Vec()
|
|
||||||
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
|
|
||||||
BRepGProp_Face(tool_face).Normal(
|
|
||||||
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
|
|
||||||
)
|
|
||||||
normalized_surface_normal = Vector(
|
|
||||||
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
|
|
||||||
).normalized()
|
|
||||||
surface_point = Vector(surface_gppnt)
|
|
||||||
ref_point = surface_point + normalized_surface_normal
|
|
||||||
|
|
||||||
# Create a HalfSpace - Solidish object to determine top/bottom
|
|
||||||
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
|
|
||||||
# mypy expects only a TopoDS_Shell here
|
|
||||||
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
|
|
||||||
# type: ignore
|
|
||||||
tool_solid = half_space_maker.Solid()
|
|
||||||
|
|
||||||
tops: list[Shape] = []
|
|
||||||
bottoms: list[Shape] = []
|
|
||||||
properties = GProp_GProps()
|
|
||||||
for part in get_top_level_topods_shapes(split_result):
|
|
||||||
sub_shape = self.__class__.cast(part)
|
|
||||||
if isinstance(tool, Plane):
|
|
||||||
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
|
|
||||||
else:
|
|
||||||
# Intersect self and the thickened tool
|
|
||||||
is_up_obj = _topods_bool_op(
|
|
||||||
(part,), (tool_solid,), BRepAlgoAPI_Common()
|
|
||||||
)
|
|
||||||
# Check for valid intersections
|
|
||||||
BRepGProp.LinearProperties_s(is_up_obj, properties)
|
|
||||||
# Mass represents the total length for linear properties
|
|
||||||
is_up = properties.Mass() >= TOLERANCE
|
|
||||||
(tops if is_up else bottoms).append(sub_shape)
|
|
||||||
|
|
||||||
top = None if not tops else tops[0] if len(tops) == 1 else tops
|
|
||||||
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
|
|
||||||
|
|
||||||
if keep == Keep.BOTH:
|
|
||||||
return (top, bottom)
|
|
||||||
if keep == Keep.TOP:
|
|
||||||
return top
|
|
||||||
if keep == Keep.BOTTOM:
|
|
||||||
return bottom
|
|
||||||
return None
|
|
||||||
|
|
||||||
def start_point(self) -> Vector:
|
def start_point(self) -> Vector:
|
||||||
"""The start point of this edge
|
"""The start point of this edge
|
||||||
|
|
||||||
|
|
@ -1549,24 +1431,24 @@ class Mixin1D(Shape):
|
||||||
"""
|
"""
|
||||||
return self.derivative_at(position, 1, position_mode).normalized()
|
return self.derivative_at(position, 1, position_mode).normalized()
|
||||||
|
|
||||||
def vertex(self) -> Vertex | None:
|
# def vertex(self) -> Vertex | None:
|
||||||
"""Return the Vertex"""
|
# """Return the Vertex"""
|
||||||
return Shape.get_single_shape(self, "Vertex")
|
# return Shape.get_single_shape(self, "Vertex")
|
||||||
|
|
||||||
def vertices(self) -> ShapeList[Vertex]:
|
# def vertices(self) -> ShapeList[Vertex]:
|
||||||
"""vertices - all the vertices in this Shape"""
|
# """vertices - all the vertices in this Shape"""
|
||||||
return Shape.get_shape_list(self, "Vertex")
|
# return Shape.get_shape_list(self, "Vertex")
|
||||||
|
|
||||||
def wire(self) -> Wire | None:
|
# def wire(self) -> Wire | None:
|
||||||
"""Return the Wire"""
|
# """Return the Wire"""
|
||||||
return Shape.get_single_shape(self, "Wire")
|
# return Shape.get_single_shape(self, "Wire")
|
||||||
|
|
||||||
def wires(self) -> ShapeList[Wire]:
|
# def wires(self) -> ShapeList[Wire]:
|
||||||
"""wires - all the wires in this Shape"""
|
# """wires - all the wires in this Shape"""
|
||||||
return Shape.get_shape_list(self, "Wire")
|
# 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
|
"""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
|
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
|
information about a curve, which could be a line, arc, or other parametrically
|
||||||
|
|
@ -1647,7 +1529,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
Returns:
|
Returns:
|
||||||
Edge: extruded shape
|
Edge: extruded shape
|
||||||
"""
|
"""
|
||||||
if obj.wrapped is None:
|
if not obj:
|
||||||
raise ValueError("Can't extrude empty vertex")
|
raise ValueError("Can't extrude empty vertex")
|
||||||
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
|
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
|
||||||
|
|
||||||
|
|
@ -2538,7 +2420,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
extension_factor: float = 0.1,
|
extension_factor: float = 0.1,
|
||||||
):
|
):
|
||||||
"""Helper method to slightly extend an edge that is bound to a surface"""
|
"""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")
|
raise ValueError("Can't extend empty spline")
|
||||||
if self.geom_type != GeomType.BSPLINE:
|
if self.geom_type != GeomType.BSPLINE:
|
||||||
raise TypeError("_extend_spline only works with splines")
|
raise TypeError("_extend_spline only works with splines")
|
||||||
|
|
@ -2595,7 +2477,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
Returns:
|
Returns:
|
||||||
ShapeList[Vector]: list of intersection points
|
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")
|
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
|
# Convert an Axis into an edge at least as large as self and Axis start point
|
||||||
|
|
@ -2723,7 +2605,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
|
|
||||||
def geom_adaptor(self) -> BRepAdaptor_Curve:
|
def geom_adaptor(self) -> BRepAdaptor_Curve:
|
||||||
"""Return the Geom Curve from this Edge"""
|
"""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")
|
raise ValueError("Can't find adaptor for empty edge")
|
||||||
return BRepAdaptor_Curve(self.wrapped)
|
return BRepAdaptor_Curve(self.wrapped)
|
||||||
|
|
||||||
|
|
@ -2811,7 +2693,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
float: Normalized parameter in [0.0, 1.0] corresponding to the point's
|
float: Normalized parameter in [0.0, 1.0] corresponding to the point's
|
||||||
closest location on the edge.
|
closest location on the edge.
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't find param on empty edge")
|
raise ValueError("Can't find param on empty edge")
|
||||||
|
|
||||||
pnt = Vector(point)
|
pnt = Vector(point)
|
||||||
|
|
@ -2945,7 +2827,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
Returns:
|
Returns:
|
||||||
Edge: reversed
|
Edge: reversed
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("An empty edge can't be reversed")
|
raise ValueError("An empty edge can't be reversed")
|
||||||
|
|
||||||
assert isinstance(self.wrapped, TopoDS_Edge)
|
assert isinstance(self.wrapped, TopoDS_Edge)
|
||||||
|
|
@ -2960,7 +2842,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
topods_edge = BRepBuilderAPI_MakeEdge(curve.Reversed(), last, first).Edge()
|
topods_edge = BRepBuilderAPI_MakeEdge(curve.Reversed(), last, first).Edge()
|
||||||
reversed_edge.wrapped = topods_edge
|
reversed_edge.wrapped = topods_edge
|
||||||
else:
|
else:
|
||||||
reversed_edge.wrapped = downcast(self.wrapped.Reversed())
|
reversed_edge.wrapped = TopoDS.Edge_s(self.wrapped.Reversed())
|
||||||
return reversed_edge
|
return reversed_edge
|
||||||
|
|
||||||
def to_axis(self) -> Axis:
|
def to_axis(self) -> Axis:
|
||||||
|
|
@ -3025,7 +2907,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
# if start_u >= end_u:
|
# if start_u >= end_u:
|
||||||
# raise ValueError(f"start ({start_u}) must be less than end ({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")
|
raise ValueError("Can't trim empty edge")
|
||||||
|
|
||||||
self_copy = copy.deepcopy(self)
|
self_copy = copy.deepcopy(self)
|
||||||
|
|
@ -3060,7 +2942,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
Returns:
|
Returns:
|
||||||
Edge: trimmed edge
|
Edge: trimmed edge
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't trim empty edge")
|
raise ValueError("Can't trim empty edge")
|
||||||
|
|
||||||
start_u = Mixin1D._to_param(self, start, "start")
|
start_u = Mixin1D._to_param(self, start, "start")
|
||||||
|
|
@ -3089,7 +2971,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
|
||||||
return Edge(new_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
|
"""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
|
of edges forming a continuous curve or path in 3D space. Wires are essential
|
||||||
components in modeling complex objects, defining boundaries for surfaces or
|
components in modeling complex objects, defining boundaries for surfaces or
|
||||||
|
|
@ -3623,7 +3505,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
Returns:
|
Returns:
|
||||||
Wire: chamfered wire
|
Wire: chamfered wire
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't chamfer empty wire")
|
raise ValueError("Can't chamfer empty wire")
|
||||||
|
|
||||||
reference_edge = edge
|
reference_edge = edge
|
||||||
|
|
@ -3638,7 +3520,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
)
|
)
|
||||||
|
|
||||||
for v in vertices:
|
for v in vertices:
|
||||||
if v.wrapped is None:
|
if not v:
|
||||||
continue
|
continue
|
||||||
edge_list = vertex_edge_map.FindFromKey(v.wrapped)
|
edge_list = vertex_edge_map.FindFromKey(v.wrapped)
|
||||||
|
|
||||||
|
|
@ -3679,6 +3561,21 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
|
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
|
def edges(self) -> ShapeList[Edge]:
|
||||||
|
"""edges - all the edges in this Shape"""
|
||||||
|
# The WireExplorer is a tool to explore the edges of a wire in a connection order.
|
||||||
|
explorer = BRepTools_WireExplorer(self.wrapped)
|
||||||
|
|
||||||
|
edge_list: ShapeList[Edge] = ShapeList()
|
||||||
|
while explorer.More():
|
||||||
|
next_edge = Edge(explorer.Current())
|
||||||
|
next_edge.topo_parent = (
|
||||||
|
self if self.topo_parent is None else self.topo_parent
|
||||||
|
)
|
||||||
|
edge_list.append(next_edge)
|
||||||
|
explorer.Next()
|
||||||
|
return edge_list
|
||||||
|
|
||||||
def fillet_2d(self, radius: float, vertices: Iterable[Vertex]) -> Wire:
|
def fillet_2d(self, radius: float, vertices: Iterable[Vertex]) -> Wire:
|
||||||
"""fillet_2d
|
"""fillet_2d
|
||||||
|
|
||||||
|
|
@ -3695,7 +3592,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
Returns:
|
Returns:
|
||||||
Wire: filleted wire
|
Wire: filleted wire
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't fillet an empty wire")
|
raise ValueError("Can't fillet an empty wire")
|
||||||
|
|
||||||
# Create a face to fillet
|
# Create a face to fillet
|
||||||
|
|
@ -3723,7 +3620,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
Returns:
|
Returns:
|
||||||
Wire: fixed wire
|
Wire: fixed wire
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't fix an empty edge")
|
raise ValueError("Can't fix an empty edge")
|
||||||
|
|
||||||
sf_w = ShapeFix_Wireframe(self.wrapped)
|
sf_w = ShapeFix_Wireframe(self.wrapped)
|
||||||
|
|
@ -3735,7 +3632,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
|
|
||||||
def geom_adaptor(self) -> BRepAdaptor_CompCurve:
|
def geom_adaptor(self) -> BRepAdaptor_CompCurve:
|
||||||
"""Return the Geom Comp Curve for this Wire"""
|
"""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")
|
raise ValueError("Can't get geom adaptor of empty wire")
|
||||||
|
|
||||||
return BRepAdaptor_CompCurve(self.wrapped)
|
return BRepAdaptor_CompCurve(self.wrapped)
|
||||||
|
|
@ -3779,7 +3676,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
float: Normalized parameter in [0.0, 1.0] representing the relative
|
float: Normalized parameter in [0.0, 1.0] representing the relative
|
||||||
position of the projected point along the wire.
|
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")
|
raise ValueError("Can't find point on empty wire")
|
||||||
|
|
||||||
point_on_curve = Vector(point)
|
point_on_curve = Vector(point)
|
||||||
|
|
@ -3932,7 +3829,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-branches
|
# 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")
|
raise ValueError("Can't project empty Wires or to empty Shapes")
|
||||||
|
|
||||||
if direction is not None and center is None:
|
if direction is not None and center is None:
|
||||||
|
|
@ -4021,7 +3918,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
Returns:
|
Returns:
|
||||||
Wire: stitched wires
|
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")
|
raise ValueError("Can't stitch empty wires")
|
||||||
|
|
||||||
wire_builder = BRepBuilderAPI_MakeWire()
|
wire_builder = BRepBuilderAPI_MakeWire()
|
||||||
|
|
@ -4065,7 +3962,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
|
||||||
"""
|
"""
|
||||||
# Build a single Geom_BSplineCurve from the wire, in *topological order*
|
# Build a single Geom_BSplineCurve from the wire, in *topological order*
|
||||||
builder = GeomConvert_CompCurveToBSplineCurve()
|
builder = GeomConvert_CompCurveToBSplineCurve()
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't convert an empty wire")
|
raise ValueError("Can't convert an empty wire")
|
||||||
wire_explorer = BRepTools_WireExplorer(self.wrapped)
|
wire_explorer = BRepTools_WireExplorer(self.wrapped)
|
||||||
|
|
||||||
|
|
@ -4217,9 +4114,9 @@ def topo_explore_connected_edges(
|
||||||
parent = parent if parent is not None else edge.topo_parent
|
parent = parent if parent is not None else edge.topo_parent
|
||||||
if parent is None:
|
if parent is None:
|
||||||
raise ValueError("edge has no valid parent")
|
raise ValueError("edge has no valid parent")
|
||||||
given_topods_edge = edge.wrapped
|
if not edge:
|
||||||
if given_topods_edge is None:
|
|
||||||
raise ValueError("edge is empty")
|
raise ValueError("edge is empty")
|
||||||
|
given_topods_edge = edge.wrapped
|
||||||
connected_edges = set()
|
connected_edges = set()
|
||||||
|
|
||||||
# Find all the TopoDS_Edges for this Shape
|
# Find all the TopoDS_Edges for this Shape
|
||||||
|
|
@ -4262,11 +4159,11 @@ def topo_explore_connected_faces(
|
||||||
) -> list[TopoDS_Face]:
|
) -> list[TopoDS_Face]:
|
||||||
"""Given an edge extracted from a Shape, return the topods_faces connected to it"""
|
"""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")
|
raise ValueError("Can't explore from an empty edge")
|
||||||
|
|
||||||
parent = parent if parent is not None else edge.topo_parent
|
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")
|
raise ValueError("edge has no valid parent")
|
||||||
|
|
||||||
# make a edge --> faces mapping
|
# make a edge --> faces mapping
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ from OCP.BRepFeat import BRepFeat_SplitShape
|
||||||
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
|
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
|
||||||
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||||
from OCP.BRepTools import BRepTools
|
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||||
|
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
||||||
from OCP.gce import gce_MakeLin
|
from OCP.gce import gce_MakeLin
|
||||||
from OCP.Geom import Geom_Line
|
from OCP.Geom import Geom_Line
|
||||||
from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf
|
from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf
|
||||||
|
|
@ -162,6 +163,7 @@ if TYPE_CHECKING: # pragma: no cover
|
||||||
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
|
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
|
||||||
TrimmingTool = Union[Plane, "Shell", "Face"]
|
TrimmingTool = Union[Plane, "Shell", "Face"]
|
||||||
TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape)
|
TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape)
|
||||||
|
CalcFn = Callable[[TopoDS_Shape, GProp_GProps], None]
|
||||||
|
|
||||||
|
|
||||||
class Shape(NodeMixin, Generic[TOPODS]):
|
class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
@ -196,7 +198,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
ta.TopAbs_COMPSOLID: "CompSolid",
|
ta.TopAbs_COMPSOLID: "CompSolid",
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_properties_LUT = {
|
shape_properties_LUT: dict[TopAbs_ShapeEnum, CalcFn | None] = {
|
||||||
ta.TopAbs_VERTEX: None,
|
ta.TopAbs_VERTEX: None,
|
||||||
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
|
||||||
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
|
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
|
||||||
|
|
@ -287,7 +289,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
color: ColorLike | None = None,
|
color: ColorLike | None = None,
|
||||||
parent: Compound | 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
|
tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None
|
||||||
)
|
)
|
||||||
self.for_construction = False
|
self.for_construction = False
|
||||||
|
|
@ -304,6 +306,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
# 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
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _dim(self) -> int | None:
|
def _dim(self) -> int | None:
|
||||||
|
|
@ -312,7 +326,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
@property
|
@property
|
||||||
def area(self) -> float:
|
def area(self) -> float:
|
||||||
"""area -the surface area of all faces in this Shape"""
|
"""area -the surface area of all faces in this Shape"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
BRepGProp.SurfaceProperties_s(self.wrapped, properties)
|
BRepGProp.SurfaceProperties_s(self.wrapped, properties)
|
||||||
|
|
@ -351,7 +365,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
GeomType: The geometry type of the shape
|
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")
|
raise ValueError("Cannot determine geometry type of an empty shape")
|
||||||
|
|
||||||
shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
|
shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
|
||||||
|
|
@ -380,7 +394,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
bool: is the shape manifold or water tight
|
bool: is the shape manifold or water tight
|
||||||
"""
|
"""
|
||||||
# Extract one or more (if a Compound) shape from self
|
# Extract one or more (if a Compound) shape from self
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return False
|
return False
|
||||||
shape_stack = get_top_level_topods_shapes(self.wrapped)
|
shape_stack = get_top_level_topods_shapes(self.wrapped)
|
||||||
|
|
||||||
|
|
@ -431,12 +445,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
underlying shape with the potential to be given a location and an
|
underlying shape with the potential to be given a location and an
|
||||||
orientation.
|
orientation.
|
||||||
"""
|
"""
|
||||||
return self.wrapped is None or self.wrapped.IsNull()
|
return self._wrapped is None or self.wrapped.IsNull()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_planar_face(self) -> bool:
|
def is_planar_face(self) -> bool:
|
||||||
"""Is the shape a planar face even though its geom_type may not be PLANE"""
|
"""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
|
return False
|
||||||
surface = BRep_Tool.Surface_s(self.wrapped)
|
surface = BRep_Tool.Surface_s(self.wrapped)
|
||||||
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
|
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
|
||||||
|
|
@ -448,7 +462,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
|
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
|
||||||
description of what is checked.
|
description of what is checked.
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return True
|
return True
|
||||||
chk = BRepCheck_Analyzer(self.wrapped)
|
chk = BRepCheck_Analyzer(self.wrapped)
|
||||||
chk.SetParallel(True)
|
chk.SetParallel(True)
|
||||||
|
|
@ -474,7 +488,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
@property
|
@property
|
||||||
def location(self) -> Location:
|
def location(self) -> Location:
|
||||||
"""Get this Shape's 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")
|
raise ValueError("Can't find the location of an empty shape")
|
||||||
return Location(self.wrapped.Location())
|
return Location(self.wrapped.Location())
|
||||||
|
|
||||||
|
|
@ -518,7 +532,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
- It is commonly used in structural analysis, mechanical simulations,
|
- It is commonly used in structural analysis, mechanical simulations,
|
||||||
and physics-based motion calculations.
|
and physics-based motion calculations.
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Can't calculate matrix for empty shape")
|
raise ValueError("Can't calculate matrix for empty shape")
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
BRepGProp.VolumeProperties_s(self.wrapped, properties)
|
BRepGProp.VolumeProperties_s(self.wrapped, properties)
|
||||||
|
|
@ -546,7 +560,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
@property
|
@property
|
||||||
def position(self) -> Vector:
|
def position(self) -> Vector:
|
||||||
"""Get the position component of this Shape's Location"""
|
"""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")
|
raise ValueError("Can't find the position of an empty shape")
|
||||||
return self.location.position
|
return self.location.position
|
||||||
|
|
||||||
|
|
@ -575,7 +589,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
(Vector(0, 1, 0), 1000.0),
|
(Vector(0, 1, 0), 1000.0),
|
||||||
(Vector(0, 0, 1), 300.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")
|
raise ValueError("Can't calculate properties for empty shape")
|
||||||
|
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
|
|
@ -615,7 +629,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
(150.0, 200.0, 50.0)
|
(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")
|
raise ValueError("Can't calculate moments for empty shape")
|
||||||
|
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
|
|
@ -785,13 +799,13 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if obj.wrapped is None:
|
if not obj:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
calc_function = Shape.shape_properties_LUT[shapetype(obj.wrapped)]
|
calc_function = Shape.shape_properties_LUT[shapetype(obj.wrapped)]
|
||||||
|
|
||||||
if not calc_function:
|
if calc_function is None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
calc_function(obj.wrapped, properties)
|
calc_function(obj.wrapped, properties)
|
||||||
|
|
@ -805,7 +819,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
],
|
],
|
||||||
) -> ShapeList:
|
) -> ShapeList:
|
||||||
"""Helper to extract entities of a specific type from a shape."""
|
"""Helper to extract entities of a specific type from a shape."""
|
||||||
if shape.wrapped is None:
|
if not shape:
|
||||||
return ShapeList()
|
return ShapeList()
|
||||||
shape_list = ShapeList(
|
shape_list = ShapeList(
|
||||||
[shape.__class__.cast(i) for i in shape.entities(entity_type)]
|
[shape.__class__.cast(i) for i in shape.entities(entity_type)]
|
||||||
|
|
@ -825,7 +839,9 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
with a warning if count != 1."""
|
with a warning if count != 1."""
|
||||||
shape_list = Shape.get_shape_list(shape, entity_type)
|
shape_list = Shape.get_shape_list(shape, entity_type)
|
||||||
entity_count = len(shape_list)
|
entity_count = len(shape_list)
|
||||||
if entity_count != 1:
|
if entity_count == 0:
|
||||||
|
return None
|
||||||
|
elif entity_count > 1:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Found {entity_count} {entity_type.lower()}s, returning first",
|
f"Found {entity_count} {entity_type.lower()}s, returning first",
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
|
|
@ -859,7 +875,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
if not all(summand._dim == addend_dim for summand in summands):
|
if not all(summand._dim == addend_dim for summand in summands):
|
||||||
raise ValueError("Only shapes with the same dimension can be added")
|
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:
|
if len(summands) == 1:
|
||||||
sum_shape = summands[0]
|
sum_shape = summands[0]
|
||||||
else:
|
else:
|
||||||
|
|
@ -876,7 +892,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
"""intersect shape with self operator &"""
|
"""intersect shape with self operator &"""
|
||||||
others = other if isinstance(other, (list, tuple)) else [other]
|
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")
|
raise ValueError("Cannot intersect shape with empty compound")
|
||||||
new_shape = self.intersect(*others)
|
new_shape = self.intersect(*others)
|
||||||
|
|
||||||
|
|
@ -948,7 +964,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""Return hash code"""
|
"""Return hash code"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return 0
|
return 0
|
||||||
return hash(self.wrapped)
|
return hash(self.wrapped)
|
||||||
|
|
||||||
|
|
@ -966,7 +982,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
|
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
|
||||||
"""cut shape from self operator -"""
|
"""cut shape from self operator -"""
|
||||||
|
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot subtract shape from empty compound")
|
raise ValueError("Cannot subtract shape from empty compound")
|
||||||
|
|
||||||
# Convert `other` to list of base objects and filter out None values
|
# Convert `other` to list of base objects and filter out None values
|
||||||
|
|
@ -1014,7 +1030,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
BoundBox: A box sized to contain this Shape
|
BoundBox: A box sized to contain this Shape
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return BoundBox(Bnd_Box())
|
return BoundBox(Bnd_Box())
|
||||||
tolerance = TOLERANCE if tolerance is None else tolerance
|
tolerance = TOLERANCE if tolerance is None else tolerance
|
||||||
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
|
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
|
||||||
|
|
@ -1033,7 +1049,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: Original object with extraneous internal edges removed
|
Shape: Original object with extraneous internal edges removed
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
|
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
|
||||||
upgrader.AllowInternalEdges(False)
|
upgrader.AllowInternalEdges(False)
|
||||||
|
|
@ -1112,7 +1128,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
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")
|
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||||
|
|
||||||
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
|
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
|
||||||
|
|
@ -1125,7 +1141,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
self, other: Shape | VectorLike
|
self, other: Shape | VectorLike
|
||||||
) -> tuple[float, Vector, Vector]:
|
) -> tuple[float, Vector, Vector]:
|
||||||
"""Minimal distance between two shapes and the points on each shape"""
|
"""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")
|
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||||
|
|
||||||
if isinstance(other, Shape):
|
if isinstance(other, Shape):
|
||||||
|
|
@ -1155,14 +1171,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot calculate distance to or from an empty shape")
|
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||||
|
|
||||||
dist_calc = BRepExtrema_DistShapeShape()
|
dist_calc = BRepExtrema_DistShapeShape()
|
||||||
dist_calc.LoadS1(self.wrapped)
|
dist_calc.LoadS1(self.wrapped)
|
||||||
|
|
||||||
for other_shape in others:
|
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")
|
raise ValueError("Cannot calculate distance to or from an empty shape")
|
||||||
dist_calc.LoadS2(other_shape.wrapped)
|
dist_calc.LoadS2(other_shape.wrapped)
|
||||||
dist_calc.Perform()
|
dist_calc.Perform()
|
||||||
|
|
@ -1171,27 +1187,28 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def edge(self) -> Edge | None:
|
def edge(self) -> Edge | None:
|
||||||
"""Return the Edge"""
|
"""Return the Edge"""
|
||||||
return None
|
return Shape.get_single_shape(self, "Edge")
|
||||||
|
|
||||||
# Note all sub-classes have vertices and vertex methods
|
|
||||||
|
|
||||||
def edges(self) -> ShapeList[Edge]:
|
def edges(self) -> ShapeList[Edge]:
|
||||||
"""edges - all the edges in this Shape - subclasses may override"""
|
"""edges - all the edges in this Shape - subclasses may override"""
|
||||||
return ShapeList()
|
edge_list = Shape.get_shape_list(self, "Edge")
|
||||||
|
return edge_list.filter_by(
|
||||||
|
lambda e: BRep_Tool.Degenerated_s(e.wrapped), reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
|
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||||
"""Return all of the TopoDS sub entities of the given type"""
|
"""Return all of the TopoDS sub entities of the given type"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return []
|
return []
|
||||||
return _topods_entities(self.wrapped, topo_type)
|
return _topods_entities(self.wrapped, topo_type)
|
||||||
|
|
||||||
def face(self) -> Face | None:
|
def face(self) -> Face | None:
|
||||||
"""Return the Face"""
|
"""Return the Face"""
|
||||||
return None
|
return Shape.get_single_shape(self, "Face")
|
||||||
|
|
||||||
def faces(self) -> ShapeList[Face]:
|
def faces(self) -> ShapeList[Face]:
|
||||||
"""faces - all the faces in this Shape"""
|
"""faces - all the faces in this Shape"""
|
||||||
return ShapeList()
|
return Shape.get_shape_list(self, "Face")
|
||||||
|
|
||||||
def faces_intersected_by_axis(
|
def faces_intersected_by_axis(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1209,7 +1226,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
list[Face]: A list of intersected faces sorted by distance from axis.position
|
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()
|
return ShapeList()
|
||||||
|
|
||||||
line = gce_MakeLin(axis.wrapped).Value()
|
line = gce_MakeLin(axis.wrapped).Value()
|
||||||
|
|
@ -1239,7 +1256,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def fix(self) -> Self:
|
def fix(self) -> Self:
|
||||||
"""fix - try to fix shape if not valid"""
|
"""fix - try to fix shape if not valid"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
if not self.is_valid:
|
if not self.is_valid:
|
||||||
shape_copy: Shape = copy.deepcopy(self, None)
|
shape_copy: Shape = copy.deepcopy(self, None)
|
||||||
|
|
@ -1281,7 +1298,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
# self, child_type: Shapes, parent_type: Shapes
|
# self, child_type: Shapes, parent_type: Shapes
|
||||||
# ) -> Dict[Shape, list[Shape]]:
|
# ) -> Dict[Shape, list[Shape]]:
|
||||||
# """This function is very slow on M1 macs and is currently unused"""
|
# """This function is very slow on M1 macs and is currently unused"""
|
||||||
# if self.wrapped is None:
|
# if self._wrapped is None:
|
||||||
# return {}
|
# return {}
|
||||||
|
|
||||||
# res = TopTools_IndexedDataMapOfShapeListOfShape()
|
# res = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||||
|
|
@ -1319,7 +1336,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
(e.g., edges, vertices) and other compounds, the method returns a list
|
(e.g., edges, vertices) and other compounds, the method returns a list
|
||||||
of only the simple shapes directly contained at the top level.
|
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()
|
||||||
return ShapeList(
|
return ShapeList(
|
||||||
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
|
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
|
||||||
|
|
@ -1327,7 +1344,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def intersect(
|
def intersect(
|
||||||
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
self, *to_intersect: Shape | Vector | Location | Axis | Plane
|
||||||
) -> None | Self | ShapeList[Self]:
|
) -> None | ShapeList[Self]:
|
||||||
"""Intersection of the arguments and this shape
|
"""Intersection of the arguments and this shape
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -1335,8 +1352,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
intersect with
|
intersect with
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Self | ShapeList[Self]: Resulting object may be of a different class than self
|
None | ShapeList[Self]: Resulting ShapeList may contain different class
|
||||||
or a ShapeList if multiple non-Compound object created
|
than self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _to_vertex(vec: Vector) -> Vertex:
|
def _to_vertex(vec: Vector) -> Vertex:
|
||||||
|
|
@ -1380,15 +1397,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
# Find the shape intersections
|
# Find the shape intersections
|
||||||
intersect_op = BRepAlgoAPI_Common()
|
intersect_op = BRepAlgoAPI_Common()
|
||||||
shape_intersections = self._bool_op((self,), objs, intersect_op)
|
intersections = self._bool_op((self,), objs, intersect_op)
|
||||||
if isinstance(shape_intersections, ShapeList) and not shape_intersections:
|
if isinstance(intersections, ShapeList):
|
||||||
return None
|
return intersections or None
|
||||||
if (
|
if isinstance(intersections, Shape) and not intersections.is_null:
|
||||||
not isinstance(shape_intersections, ShapeList)
|
return ShapeList([intersections])
|
||||||
and shape_intersections.is_null
|
return None
|
||||||
):
|
|
||||||
return None
|
|
||||||
return shape_intersections
|
|
||||||
|
|
||||||
def is_equal(self, other: Shape) -> bool:
|
def is_equal(self, other: Shape) -> bool:
|
||||||
"""Returns True if two shapes are equal, i.e. if they share the same
|
"""Returns True if two shapes are equal, i.e. if they share the same
|
||||||
|
|
@ -1401,7 +1415,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None or other.wrapped is None:
|
if self._wrapped is None or not other:
|
||||||
return False
|
return False
|
||||||
return self.wrapped.IsEqual(other.wrapped)
|
return self.wrapped.IsEqual(other.wrapped)
|
||||||
|
|
||||||
|
|
@ -1416,7 +1430,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None or other.wrapped is None:
|
if self._wrapped is None or not other:
|
||||||
return False
|
return False
|
||||||
return self.wrapped.IsSame(other.wrapped)
|
return self.wrapped.IsSame(other.wrapped)
|
||||||
|
|
||||||
|
|
@ -1429,7 +1443,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot locate an empty shape")
|
raise ValueError("Cannot locate an empty shape")
|
||||||
if loc.wrapped is None:
|
if loc.wrapped is None:
|
||||||
raise ValueError("Cannot locate a shape at an empty location")
|
raise ValueError("Cannot locate a shape at an empty location")
|
||||||
|
|
@ -1448,7 +1462,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: copy of Shape at location
|
Shape: copy of Shape at location
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot locate an empty shape")
|
raise ValueError("Cannot locate an empty shape")
|
||||||
if loc.wrapped is None:
|
if loc.wrapped is None:
|
||||||
raise ValueError("Cannot locate a shape at an empty location")
|
raise ValueError("Cannot locate a shape at an empty location")
|
||||||
|
|
@ -1466,7 +1480,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot mesh an empty shape")
|
raise ValueError("Cannot mesh an empty shape")
|
||||||
|
|
||||||
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
|
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
|
||||||
|
|
@ -1487,7 +1501,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
if not mirror_plane:
|
if not mirror_plane:
|
||||||
mirror_plane = Plane.XY
|
mirror_plane = Plane.XY
|
||||||
|
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
transformation = gp_Trsf()
|
transformation = gp_Trsf()
|
||||||
transformation.SetMirror(
|
transformation.SetMirror(
|
||||||
|
|
@ -1505,7 +1519,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot move an empty shape")
|
raise ValueError("Cannot move an empty shape")
|
||||||
if loc.wrapped is None:
|
if loc.wrapped is None:
|
||||||
raise ValueError("Cannot move a shape at an empty location")
|
raise ValueError("Cannot move a shape at an empty location")
|
||||||
|
|
@ -1525,7 +1539,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: copy of Shape moved to relative location
|
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")
|
raise ValueError("Cannot move an empty shape")
|
||||||
if loc.wrapped is None:
|
if loc.wrapped is None:
|
||||||
raise ValueError("Cannot move a shape at an empty location")
|
raise ValueError("Cannot move a shape at an empty location")
|
||||||
|
|
@ -1539,7 +1553,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
OrientedBoundBox: A box oriented and sized to contain this Shape
|
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(Bnd_OBB())
|
||||||
return OrientedBoundBox(self)
|
return OrientedBoundBox(self)
|
||||||
|
|
||||||
|
|
@ -1641,7 +1655,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
- The radius of gyration is computed based on the shape’s mass properties.
|
- The radius of gyration is computed based on the shape’s mass properties.
|
||||||
- It is useful for evaluating structural stability and rotational behavior.
|
- 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")
|
raise ValueError("Can't calculate radius of gyration for empty shape")
|
||||||
|
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
|
|
@ -1660,7 +1674,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot relocate an empty shape")
|
raise ValueError("Cannot relocate an empty shape")
|
||||||
if loc.wrapped is None:
|
if loc.wrapped is None:
|
||||||
raise ValueError("Cannot relocate a shape at an empty location")
|
raise ValueError("Cannot relocate a shape at an empty location")
|
||||||
|
|
@ -1713,11 +1727,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def shell(self) -> Shell | None:
|
def shell(self) -> Shell | None:
|
||||||
"""Return the Shell"""
|
"""Return the Shell"""
|
||||||
return None
|
return Shape.get_single_shape(self, "Shell")
|
||||||
|
|
||||||
def shells(self) -> ShapeList[Shell]:
|
def shells(self) -> ShapeList[Shell]:
|
||||||
"""shells - all the shells in this Shape"""
|
"""shells - all the shells in this Shape"""
|
||||||
return ShapeList()
|
return Shape.get_shape_list(self, "Shell")
|
||||||
|
|
||||||
def show_topology(
|
def show_topology(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1771,11 +1785,157 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def solid(self) -> Solid | None:
|
def solid(self) -> Solid | None:
|
||||||
"""Return the Solid"""
|
"""Return the Solid"""
|
||||||
return None
|
return Shape.get_single_shape(self, "Solid")
|
||||||
|
|
||||||
def solids(self) -> ShapeList[Solid]:
|
def solids(self) -> ShapeList[Solid]:
|
||||||
"""solids - all the solids in this Shape"""
|
"""solids - all the solids in this Shape"""
|
||||||
return ShapeList()
|
return Shape.get_shape_list(self, "Solid")
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def split(
|
||||||
|
self, tool: TrimmingTool, keep: Literal[Keep.TOP, Keep.BOTTOM]
|
||||||
|
) -> Self | list[Self] | None:
|
||||||
|
"""split and keep inside or outside"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def split(self, tool: TrimmingTool, keep: Literal[Keep.ALL]) -> list[Self]:
|
||||||
|
"""split and return the unordered pieces"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def split(self, tool: TrimmingTool, keep: Literal[Keep.BOTH]) -> tuple[
|
||||||
|
Self | list[Self] | None,
|
||||||
|
Self | list[Self] | None,
|
||||||
|
]:
|
||||||
|
"""split and keep inside and outside"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def split(
|
||||||
|
self, tool: TrimmingTool, keep: Literal[Keep.INSIDE, Keep.OUTSIDE]
|
||||||
|
) -> None:
|
||||||
|
"""invalid split"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def split(self, tool: TrimmingTool) -> Self | list[Self] | None:
|
||||||
|
"""split and keep inside (default)"""
|
||||||
|
|
||||||
|
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
||||||
|
"""split
|
||||||
|
|
||||||
|
Split this shape by the provided plane or face.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surface (Plane | Face): surface to segment shape
|
||||||
|
keep (Keep, optional): which object(s) to save. Defaults to Keep.TOP.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Shape: result of split
|
||||||
|
Returns:
|
||||||
|
Self | list[Self] | None,
|
||||||
|
Tuple[Self | list[Self] | None]: The result of the split operation.
|
||||||
|
|
||||||
|
- **Keep.TOP**: Returns the top as a `Self` or `list[Self]`, or `None`
|
||||||
|
if no top is found.
|
||||||
|
- **Keep.BOTTOM**: Returns the bottom as a `Self` or `list[Self]`, or `None`
|
||||||
|
if no bottom is found.
|
||||||
|
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
|
||||||
|
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
|
||||||
|
"""
|
||||||
|
if self._wrapped is None or not tool:
|
||||||
|
raise ValueError("Can't split an empty edge/wire/tool")
|
||||||
|
|
||||||
|
if keep in [Keep.INSIDE, Keep.OUTSIDE]:
|
||||||
|
raise ValueError(f"{keep} is invalid")
|
||||||
|
|
||||||
|
shape_list = TopTools_ListOfShape()
|
||||||
|
shape_list.Append(self.wrapped)
|
||||||
|
|
||||||
|
# Define the splitting tool
|
||||||
|
trim_tool = (
|
||||||
|
BRepBuilderAPI_MakeFace(tool.wrapped).Face() # gp_Pln to Face
|
||||||
|
if isinstance(tool, Plane)
|
||||||
|
else tool.wrapped
|
||||||
|
)
|
||||||
|
tool_list = TopTools_ListOfShape()
|
||||||
|
tool_list.Append(trim_tool)
|
||||||
|
|
||||||
|
# Create the splitter algorithm
|
||||||
|
splitter = BRepAlgoAPI_Splitter()
|
||||||
|
|
||||||
|
# Set the shape to be split and the splitting tool (plane face)
|
||||||
|
splitter.SetArguments(shape_list)
|
||||||
|
splitter.SetTools(tool_list)
|
||||||
|
|
||||||
|
# Perform the splitting operation
|
||||||
|
splitter.Build()
|
||||||
|
|
||||||
|
split_result = downcast(splitter.Shape())
|
||||||
|
# Remove unnecessary TopoDS_Compound around single shape
|
||||||
|
if isinstance(split_result, TopoDS_Compound):
|
||||||
|
split_result = unwrap_topods_compound(split_result, True)
|
||||||
|
|
||||||
|
# For speed the user may just want all the objects which they
|
||||||
|
# can sort more efficiently then the generic algorithm below
|
||||||
|
if keep == Keep.ALL:
|
||||||
|
return ShapeList(
|
||||||
|
self.__class__.cast(part)
|
||||||
|
for part in get_top_level_topods_shapes(split_result)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(tool, Plane):
|
||||||
|
# Get a TopoDS_Face to work with from the tool
|
||||||
|
if isinstance(trim_tool, TopoDS_Shell):
|
||||||
|
face_explorer = TopExp_Explorer(trim_tool, ta.TopAbs_FACE)
|
||||||
|
tool_face = TopoDS.Face_s(face_explorer.Current())
|
||||||
|
else:
|
||||||
|
tool_face = trim_tool
|
||||||
|
|
||||||
|
# Create a reference point off the +ve side of the tool
|
||||||
|
surface_gppnt = gp_Pnt()
|
||||||
|
surface_normal = gp_Vec()
|
||||||
|
u_min, u_max, v_min, v_max = BRepTools.UVBounds_s(tool_face)
|
||||||
|
BRepGProp_Face(tool_face).Normal(
|
||||||
|
(u_min + u_max) / 2, (v_min + v_max) / 2, surface_gppnt, surface_normal
|
||||||
|
)
|
||||||
|
normalized_surface_normal = Vector(
|
||||||
|
surface_normal.X(), surface_normal.Y(), surface_normal.Z()
|
||||||
|
).normalized()
|
||||||
|
surface_point = Vector(surface_gppnt)
|
||||||
|
ref_point = surface_point + normalized_surface_normal
|
||||||
|
|
||||||
|
# Create a HalfSpace - Solidish object to determine top/bottom
|
||||||
|
# Note: BRepPrimAPI_MakeHalfSpace takes either a TopoDS_Shell or TopoDS_Face but the
|
||||||
|
# mypy expects only a TopoDS_Shell here
|
||||||
|
half_space_maker = BRepPrimAPI_MakeHalfSpace(trim_tool, ref_point.to_pnt())
|
||||||
|
# type: ignore
|
||||||
|
tool_solid = half_space_maker.Solid()
|
||||||
|
|
||||||
|
tops: list[Shape] = []
|
||||||
|
bottoms: list[Shape] = []
|
||||||
|
properties = GProp_GProps()
|
||||||
|
for part in get_top_level_topods_shapes(split_result):
|
||||||
|
sub_shape = self.__class__.cast(part)
|
||||||
|
if isinstance(tool, Plane):
|
||||||
|
is_up = tool.to_local_coords(sub_shape).center().Z >= 0
|
||||||
|
else:
|
||||||
|
# Intersect self and the thickened tool
|
||||||
|
is_up_obj = _topods_bool_op(
|
||||||
|
(part,), (tool_solid,), BRepAlgoAPI_Common()
|
||||||
|
)
|
||||||
|
# Check for valid intersections
|
||||||
|
BRepGProp.LinearProperties_s(is_up_obj, properties)
|
||||||
|
# Mass represents the total length for linear properties
|
||||||
|
is_up = properties.Mass() >= TOLERANCE
|
||||||
|
(tops if is_up else bottoms).append(sub_shape)
|
||||||
|
|
||||||
|
top = None if not tops else tops[0] if len(tops) == 1 else tops
|
||||||
|
bottom = None if not bottoms else bottoms[0] if len(bottoms) == 1 else bottoms
|
||||||
|
|
||||||
|
if keep == Keep.BOTH:
|
||||||
|
return (top, bottom)
|
||||||
|
if keep == Keep.TOP:
|
||||||
|
return top
|
||||||
|
if keep == Keep.BOTTOM:
|
||||||
|
return bottom
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def split_by_perimeter(
|
def split_by_perimeter(
|
||||||
|
|
@ -1855,7 +2015,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
"keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH"
|
"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")
|
raise ValueError("Cannot split an empty shape")
|
||||||
|
|
||||||
# Process the perimeter
|
# Process the perimeter
|
||||||
|
|
@ -1863,7 +2023,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
raise ValueError("perimeter must be a closed Wire or Edge")
|
raise ValueError("perimeter must be a closed Wire or Edge")
|
||||||
perimeter_edges = TopTools_SequenceOfShape()
|
perimeter_edges = TopTools_SequenceOfShape()
|
||||||
for perimeter_edge in perimeter.edges():
|
for perimeter_edge in perimeter.edges():
|
||||||
if perimeter_edge.wrapped is None:
|
if not perimeter_edge:
|
||||||
continue
|
continue
|
||||||
perimeter_edges.Append(perimeter_edge.wrapped)
|
perimeter_edges.Append(perimeter_edge.wrapped)
|
||||||
|
|
||||||
|
|
@ -1871,7 +2031,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
lefts: list[Shell] = []
|
lefts: list[Shell] = []
|
||||||
rights: list[Shell] = []
|
rights: list[Shell] = []
|
||||||
for target_shell in self.shells():
|
for target_shell in self.shells():
|
||||||
if target_shell.wrapped is None:
|
if not target_shell:
|
||||||
continue
|
continue
|
||||||
constructor = BRepFeat_SplitShape(target_shell.wrapped)
|
constructor = BRepFeat_SplitShape(target_shell.wrapped)
|
||||||
constructor.Add(perimeter_edges)
|
constructor.Add(perimeter_edges)
|
||||||
|
|
@ -1900,7 +2060,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
self, tolerance: float, angular_tolerance: float = 0.1
|
self, tolerance: float, angular_tolerance: float = 0.1
|
||||||
) -> tuple[list[Vector], list[tuple[int, int, int]]]:
|
) -> tuple[list[Vector], list[tuple[int, int, int]]]:
|
||||||
"""General triangulated approximation"""
|
"""General triangulated approximation"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot tessellate an empty shape")
|
raise ValueError("Cannot tessellate an empty shape")
|
||||||
|
|
||||||
self.mesh(tolerance, angular_tolerance)
|
self.mesh(tolerance, angular_tolerance)
|
||||||
|
|
@ -1962,7 +2122,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Self: Approximated shape
|
Self: Approximated shape
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot approximate an empty shape")
|
raise ValueError("Cannot approximate an empty shape")
|
||||||
|
|
||||||
params = ShapeCustom_RestrictionParameters()
|
params = ShapeCustom_RestrictionParameters()
|
||||||
|
|
@ -1999,7 +2159,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: a copy of the object, but with geometry transformed
|
Shape: a copy of the object, but with geometry transformed
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
new_shape = copy.deepcopy(self, None)
|
new_shape = copy.deepcopy(self, None)
|
||||||
transformed = downcast(
|
transformed = downcast(
|
||||||
|
|
@ -2022,7 +2182,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: copy of transformed shape with all objects keeping their type
|
Shape: copy of transformed shape with all objects keeping their type
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
new_shape = copy.deepcopy(self, None)
|
new_shape = copy.deepcopy(self, None)
|
||||||
transformed = downcast(
|
transformed = downcast(
|
||||||
|
|
@ -2078,11 +2238,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
def wire(self) -> Wire | None:
|
def wire(self) -> Wire | None:
|
||||||
"""Return the Wire"""
|
"""Return the Wire"""
|
||||||
return None
|
return Shape.get_single_shape(self, "Wire")
|
||||||
|
|
||||||
def wires(self) -> ShapeList[Wire]:
|
def wires(self) -> ShapeList[Wire]:
|
||||||
"""wires - all the wires in this Shape"""
|
"""wires - all the wires in this Shape"""
|
||||||
return ShapeList()
|
return Shape.get_shape_list(self, "Wire")
|
||||||
|
|
||||||
def _apply_transform(self, transformation: gp_Trsf) -> Self:
|
def _apply_transform(self, transformation: gp_Trsf) -> Self:
|
||||||
"""Private Apply Transform
|
"""Private Apply Transform
|
||||||
|
|
@ -2095,7 +2255,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
Shape: copy of transformed Shape
|
Shape: copy of transformed Shape
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return self
|
return self
|
||||||
shape_copy: Shape = copy.deepcopy(self, None)
|
shape_copy: Shape = copy.deepcopy(self, None)
|
||||||
transformed_shape = BRepBuilderAPI_Transform(
|
transformed_shape = BRepBuilderAPI_Transform(
|
||||||
|
|
@ -2126,7 +2286,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
args = list(args)
|
args = list(args)
|
||||||
tools = list(tools)
|
tools = list(tools)
|
||||||
# Find the highest order class from all the inputs Solid > Vertex
|
# 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]
|
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
|
||||||
|
|
||||||
# The base of the operation
|
# The base of the operation
|
||||||
|
|
@ -2200,7 +2364,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
Returns:
|
Returns:
|
||||||
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
|
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())
|
return (ShapeList(), ShapeList())
|
||||||
|
|
||||||
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
|
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
|
||||||
|
|
@ -2234,6 +2398,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
|
||||||
|
|
||||||
return shape_to_html(self)._repr_html_()
|
return shape_to_html(self)._repr_html_()
|
||||||
|
|
||||||
|
def vertex(self) -> Vertex | None:
|
||||||
|
"""Return the Vertex"""
|
||||||
|
return Shape.get_single_shape(self, "Vertex")
|
||||||
|
|
||||||
|
def vertices(self) -> ShapeList[Vertex]:
|
||||||
|
"""vertices - all the vertices in this Shape"""
|
||||||
|
return Shape.get_shape_list(self, "Vertex")
|
||||||
|
|
||||||
|
|
||||||
class Comparable(ABC):
|
class Comparable(ABC):
|
||||||
"""Abstract base class that requires comparison methods"""
|
"""Abstract base class that requires comparison methods"""
|
||||||
|
|
@ -2701,15 +2873,16 @@ class ShapeList(list[T]):
|
||||||
tol_digits,
|
tol_digits,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif hasattr(group_by, "wrapped"):
|
elif not group_by:
|
||||||
if group_by.wrapped is None:
|
raise ValueError("Cannot group by an empty object")
|
||||||
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):
|
def key_f(obj):
|
||||||
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
pnt1, _pnt2 = group_by.closest_points(obj.center())
|
||||||
return round(group_by.param_at_point(pnt1), tol_digits)
|
return round(group_by.param_at_point(pnt1), tol_digits)
|
||||||
|
|
||||||
elif isinstance(group_by, SortBy):
|
elif isinstance(group_by, SortBy):
|
||||||
if group_by == SortBy.LENGTH:
|
if group_by == SortBy.LENGTH:
|
||||||
|
|
@ -2815,22 +2988,22 @@ class ShapeList(list[T]):
|
||||||
).position.Z,
|
).position.Z,
|
||||||
reverse=reverse,
|
reverse=reverse,
|
||||||
)
|
)
|
||||||
elif hasattr(sort_by, "wrapped"):
|
elif not sort_by:
|
||||||
if sort_by.wrapped is None:
|
raise ValueError("Cannot sort by an empty object")
|
||||||
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:
|
# pylint: disable=unnecessary-lambda
|
||||||
"""u-value of closest point between object center and sort_by"""
|
objects = sorted(
|
||||||
assert not isinstance(sort_by, SortBy)
|
self, key=lambda o: u_of_closest_center(o), reverse=reverse
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(sort_by, SortBy):
|
elif isinstance(sort_by, SortBy):
|
||||||
if sort_by == SortBy.LENGTH:
|
if sort_by == SortBy.LENGTH:
|
||||||
|
|
@ -2997,6 +3170,45 @@ def _sew_topods_faces(faces: Iterable[TopoDS_Face]) -> TopoDS_Shape:
|
||||||
return downcast(shell_builder.SewedShape())
|
return downcast(shell_builder.SewedShape())
|
||||||
|
|
||||||
|
|
||||||
|
def _topods_bool_op(
|
||||||
|
args: Iterable[TopoDS_Shape],
|
||||||
|
tools: Iterable[TopoDS_Shape],
|
||||||
|
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
|
||||||
|
) -> TopoDS_Shape:
|
||||||
|
"""Generic boolean operation for TopoDS_Shapes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Iterable[TopoDS_Shape]:
|
||||||
|
tools: Iterable[TopoDS_Shape]:
|
||||||
|
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
|
||||||
|
|
||||||
|
Returns: TopoDS_Shape
|
||||||
|
|
||||||
|
"""
|
||||||
|
args = list(args)
|
||||||
|
tools = list(tools)
|
||||||
|
arg = TopTools_ListOfShape()
|
||||||
|
for obj in args:
|
||||||
|
arg.Append(obj)
|
||||||
|
|
||||||
|
tool = TopTools_ListOfShape()
|
||||||
|
for obj in tools:
|
||||||
|
tool.Append(obj)
|
||||||
|
|
||||||
|
operation.SetArguments(arg)
|
||||||
|
operation.SetTools(tool)
|
||||||
|
|
||||||
|
operation.SetRunParallel(True)
|
||||||
|
operation.Build()
|
||||||
|
|
||||||
|
result = downcast(operation.Shape())
|
||||||
|
# Remove unnecessary TopoDS_Compound around single shape
|
||||||
|
if isinstance(result, TopoDS_Compound):
|
||||||
|
result = unwrap_topods_compound(result, True)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]:
|
def _topods_entities(shape: TopoDS_Shape, topo_type: Shapes) -> list[TopoDS_Shape]:
|
||||||
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
"""Return the TopoDS_Shapes of topo_type from this TopoDS_Shape"""
|
||||||
out = {} # using dict to prevent duplicates
|
out = {} # using dict to prevent duplicates
|
||||||
|
|
|
||||||
|
|
@ -54,15 +54,13 @@ license:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
from collections.abc import Iterable, Sequence
|
||||||
import warnings
|
|
||||||
from math import radians, cos, tan
|
from math import radians, cos, tan
|
||||||
from typing import Union, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
from typing_extensions import Self
|
||||||
from collections.abc import Iterable
|
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
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.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
|
||||||
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
|
||||||
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
from OCP.BRepFeat import BRepFeat_MakeDPrism
|
||||||
|
|
@ -86,15 +84,23 @@ from OCP.GProp import GProp_GProps
|
||||||
from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
|
from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_JoinType
|
||||||
from OCP.LocOpe import LocOpe_DPrism
|
from OCP.LocOpe import LocOpe_DPrism
|
||||||
from OCP.ShapeFix import ShapeFix_Solid
|
from OCP.ShapeFix import ShapeFix_Solid
|
||||||
from OCP.Standard import Standard_Failure
|
from OCP.Standard import Standard_Failure, Standard_TypeMismatch
|
||||||
from OCP.StdFail import StdFail_NotDone
|
from OCP.StdFail import StdFail_NotDone
|
||||||
from OCP.TopExp import TopExp
|
from OCP.TopExp import TopExp, TopExp_Explorer
|
||||||
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
|
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
|
||||||
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire
|
from OCP.TopoDS import (
|
||||||
|
TopoDS,
|
||||||
|
TopoDS_Face,
|
||||||
|
TopoDS_Shape,
|
||||||
|
TopoDS_Shell,
|
||||||
|
TopoDS_Solid,
|
||||||
|
TopoDS_Wire,
|
||||||
|
)
|
||||||
from OCP.gp import gp_Ax2, gp_Pnt
|
from OCP.gp import gp_Ax2, gp_Pnt
|
||||||
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
|
from build123d.build_enums import CenterOf, GeomType, Keep, Kind, Transition, Until
|
||||||
from build123d.geometry import (
|
from build123d.geometry import (
|
||||||
DEG2RAD,
|
DEG2RAD,
|
||||||
|
TOLERANCE,
|
||||||
Axis,
|
Axis,
|
||||||
BoundBox,
|
BoundBox,
|
||||||
Color,
|
Color,
|
||||||
|
|
@ -104,10 +110,20 @@ from build123d.geometry import (
|
||||||
Vector,
|
Vector,
|
||||||
VectorLike,
|
VectorLike,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from .one_d import Edge, Wire, Mixin1D
|
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,
|
||||||
|
_sew_topods_faces,
|
||||||
|
get_top_level_topods_shapes,
|
||||||
|
unwrap_topods_compound,
|
||||||
|
)
|
||||||
|
|
||||||
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
|
||||||
from .utils import (
|
from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
|
|
@ -119,26 +135,14 @@ from .zero_d import Vertex
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801
|
from .composite import Compound # pylint: disable=R0801
|
||||||
|
|
||||||
|
|
||||||
class Mixin3D(Shape):
|
class Mixin3D(Shape[TOPODS]):
|
||||||
"""Additional methods to add to 3D Shape classes"""
|
"""Additional methods to add to 3D Shape classes"""
|
||||||
|
|
||||||
project_to_viewport = Mixin1D.project_to_viewport
|
|
||||||
split = Mixin1D.split
|
|
||||||
find_intersection_points = Mixin2D.find_intersection_points
|
find_intersection_points = Mixin2D.find_intersection_points
|
||||||
|
|
||||||
vertices = Mixin1D.vertices
|
|
||||||
vertex = Mixin1D.vertex
|
|
||||||
edges = Mixin1D.edges
|
|
||||||
edge = Mixin1D.edge
|
|
||||||
wires = Mixin1D.wires
|
|
||||||
wire = Mixin1D.wire
|
|
||||||
faces = Mixin2D.faces
|
|
||||||
face = Mixin2D.face
|
|
||||||
shells = Mixin2D.shells
|
|
||||||
shell = Mixin2D.shell
|
|
||||||
# ---- Properties ----
|
# ---- Properties ----
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -195,6 +199,7 @@ class Mixin3D(Shape):
|
||||||
if center_of == CenterOf.MASS:
|
if center_of == CenterOf.MASS:
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
calc_function = Shape.shape_properties_LUT[shapetype(self.wrapped)]
|
calc_function = Shape.shape_properties_LUT[shapetype(self.wrapped)]
|
||||||
|
assert calc_function is not None
|
||||||
calc_function(self.wrapped, properties)
|
calc_function(self.wrapped, properties)
|
||||||
middle = Vector(properties.CentreOfMass())
|
middle = Vector(properties.CentreOfMass())
|
||||||
elif center_of == CenterOf.BOUNDING_BOX:
|
elif center_of == CenterOf.BOUNDING_BOX:
|
||||||
|
|
@ -420,6 +425,132 @@ class Mixin3D(Shape):
|
||||||
|
|
||||||
return return_value
|
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 | BRepAlgoAPI_Common = (
|
||||||
|
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:
|
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
|
||||||
"""Returns whether or not the point is inside a solid or compound
|
"""Returns whether or not the point is inside a solid or compound
|
||||||
object within the specified tolerance.
|
object within the specified tolerance.
|
||||||
|
|
@ -486,8 +617,10 @@ class Mixin3D(Shape):
|
||||||
try:
|
try:
|
||||||
new_shape = self.__class__(fillet_builder.Shape())
|
new_shape = self.__class__(fillet_builder.Shape())
|
||||||
if not new_shape.is_valid:
|
if not new_shape.is_valid:
|
||||||
raise fillet_exception
|
# raise fillet_exception
|
||||||
except fillet_exception:
|
raise Standard_Failure
|
||||||
|
# except fillet_exception:
|
||||||
|
except (Standard_Failure, StdFail_NotDone):
|
||||||
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
return __max_fillet(window_min, window_mid, current_iteration + 1)
|
||||||
|
|
||||||
# These numbers work, are they close enough? - if not try larger window
|
# These numbers work, are they close enough? - if not try larger window
|
||||||
|
|
@ -506,10 +639,10 @@ class Mixin3D(Shape):
|
||||||
|
|
||||||
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
|
# Unfortunately, MacOS doesn't support the StdFail_NotDone exception so platform
|
||||||
# specific exceptions are required.
|
# specific exceptions are required.
|
||||||
if platform.system() == "Darwin":
|
# if platform.system() == "Darwin":
|
||||||
fillet_exception = Standard_Failure
|
# fillet_exception = Standard_Failure
|
||||||
else:
|
# else:
|
||||||
fillet_exception = StdFail_NotDone
|
# fillet_exception = StdFail_NotDone
|
||||||
|
|
||||||
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
|
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
|
||||||
|
|
||||||
|
|
@ -581,16 +714,35 @@ class Mixin3D(Shape):
|
||||||
|
|
||||||
return offset_solid
|
return offset_solid
|
||||||
|
|
||||||
def solid(self) -> Solid | None:
|
def project_to_viewport(
|
||||||
"""Return the Solid"""
|
self,
|
||||||
return Shape.get_single_shape(self, "Solid")
|
viewport_origin: VectorLike,
|
||||||
|
viewport_up: VectorLike = (0, 0, 1),
|
||||||
|
look_at: VectorLike | None = None,
|
||||||
|
focus: float | None = None,
|
||||||
|
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
|
||||||
|
"""project_to_viewport
|
||||||
|
|
||||||
def solids(self) -> ShapeList[Solid]:
|
Project a shape onto a viewport returning visible and hidden Edges.
|
||||||
"""solids - all the solids in this Shape"""
|
|
||||||
return Shape.get_shape_list(self, "Solid")
|
Args:
|
||||||
|
viewport_origin (VectorLike): location of viewport
|
||||||
|
viewport_up (VectorLike, optional): direction of the viewport y axis.
|
||||||
|
Defaults to (0, 0, 1).
|
||||||
|
look_at (VectorLike, optional): point to look at.
|
||||||
|
Defaults to None (center of shape).
|
||||||
|
focus (float, optional): the focal length for perspective projection
|
||||||
|
Defaults to None (orthographic projection)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
|
||||||
|
"""
|
||||||
|
return Mixin1D.project_to_viewport(
|
||||||
|
self, viewport_origin, viewport_up, look_at, focus
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
class Solid(Mixin3D[TopoDS_Solid]):
|
||||||
"""A Solid in build123d represents a three-dimensional solid geometry
|
"""A Solid in build123d represents a three-dimensional solid geometry
|
||||||
in a topological structure. A solid is a closed and bounded volume, enclosing
|
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
|
a region in 3D space. It comprises faces, edges, and vertices connected in a
|
||||||
|
|
@ -768,7 +920,17 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
inner_comp = _make_topods_compound_from_shapes(inner_solids)
|
inner_comp = _make_topods_compound_from_shapes(inner_solids)
|
||||||
|
|
||||||
# subtract from the outer solid
|
# subtract from the outer solid
|
||||||
return Solid(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
|
difference = BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape()
|
||||||
|
|
||||||
|
# convert to a TopoDS_Solid - might be wrapped in a TopoDS_Compound
|
||||||
|
try:
|
||||||
|
result = TopoDS.Solid_s(difference)
|
||||||
|
except Standard_TypeMismatch:
|
||||||
|
result = TopoDS.Solid_s(
|
||||||
|
unwrap_topods_compound(TopoDS.Compound_s(difference), True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Solid(result)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extrude_taper(
|
def extrude_taper(
|
||||||
|
|
@ -809,7 +971,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
direction.length / cos(radians(taper)),
|
direction.length / cos(radians(taper)),
|
||||||
radians(taper),
|
radians(taper),
|
||||||
)
|
)
|
||||||
new_solid = Solid(prism_builder.Shape())
|
new_solid = Solid(TopoDS.Solid_s(prism_builder.Shape()))
|
||||||
else:
|
else:
|
||||||
# Determine the offset to get the taper
|
# Determine the offset to get the taper
|
||||||
offset_amt = -direction.length * tan(radians(taper))
|
offset_amt = -direction.length * tan(radians(taper))
|
||||||
|
|
@ -848,110 +1010,116 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def extrude_until(
|
def extrude_until(
|
||||||
cls,
|
cls,
|
||||||
section: Face,
|
profile: Face,
|
||||||
target_object: Compound | Solid,
|
target: Compound | Solid,
|
||||||
direction: VectorLike,
|
direction: VectorLike,
|
||||||
until: Until = Until.NEXT,
|
until: Until = Until.NEXT,
|
||||||
) -> Compound | Solid:
|
) -> Solid:
|
||||||
"""extrude_until
|
"""extrude_until
|
||||||
|
|
||||||
Extrude section in provided direction until it encounters either the
|
Extrude `profile` in the provided `direction` until it encounters a
|
||||||
NEXT or LAST surface of target_object. Note that the bounding surface
|
bounding surface on the `target`. The termination surface is chosen
|
||||||
must be larger than the extruded face where they contact.
|
according to the `until` option:
|
||||||
|
|
||||||
|
* ``Until.NEXT`` — Extrude forward until the first intersecting surface.
|
||||||
|
* ``Until.LAST`` — Extrude forward through all intersections, stopping at
|
||||||
|
the farthest surface.
|
||||||
|
* ``Until.PREVIOUS`` — Reverse the extrusion direction and stop at the
|
||||||
|
first intersecting surface behind the profile.
|
||||||
|
* ``Until.FIRST`` — Reverse the direction and stop at the farthest
|
||||||
|
surface behind the profile.
|
||||||
|
|
||||||
|
When ``Until.PREVIOUS`` or ``Until.FIRST`` are used, the extrusion
|
||||||
|
direction is automatically inverted before execution.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The bounding surface on the target must be large enough to
|
||||||
|
completely cover the extruded profile at the contact region.
|
||||||
|
Partial overlaps may yield open or invalid solids.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
section (Face): Face to extrude
|
profile (Face): The face to extrude.
|
||||||
target_object (Union[Compound, Solid]): object to limit extrusion
|
target (Union[Compound, Solid]): The object that limits the extrusion.
|
||||||
direction (VectorLike): extrusion direction
|
direction (VectorLike): Extrusion direction.
|
||||||
until (Until, optional): surface to limit extrusion. Defaults to Until.NEXT.
|
until (Until, optional): Surface selection mode controlling which
|
||||||
|
intersection to stop at. Defaults to ``Until.NEXT``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: provided face does not intersect target_object
|
ValueError: If the provided profile does not intersect the target.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Union[Compound, Solid]: extruded Face
|
Solid: The extruded and limited solid.
|
||||||
"""
|
"""
|
||||||
direction = Vector(direction)
|
direction = Vector(direction)
|
||||||
if until in [Until.PREVIOUS, Until.FIRST]:
|
if until in [Until.PREVIOUS, Until.FIRST]:
|
||||||
direction *= -1
|
direction *= -1
|
||||||
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
until = Until.NEXT if until == Until.PREVIOUS else Until.LAST
|
||||||
|
|
||||||
max_dimension = find_max_dimension([section, target_object])
|
# 1: Create extrusion of length the maximum distance between profile and target
|
||||||
clipping_direction = (
|
max_dimension = find_max_dimension([profile, target])
|
||||||
direction * max_dimension
|
extrusion = Solid.extrude(profile, direction * max_dimension)
|
||||||
if until == Until.NEXT
|
|
||||||
else -direction * max_dimension
|
# 2: Intersect the extrusion with the target to find the target's modified faces
|
||||||
|
intersect_op = BRepAlgoAPI_Common(target.wrapped, extrusion.wrapped)
|
||||||
|
intersect_op.Build()
|
||||||
|
intersection = intersect_op.Shape()
|
||||||
|
face_exp = TopExp_Explorer(intersection, ta.TopAbs_FACE)
|
||||||
|
if not face_exp.More():
|
||||||
|
raise ValueError("No intersection: extrusion does not contact target")
|
||||||
|
|
||||||
|
# Find the faces from the intersection that originated on the target
|
||||||
|
history = intersect_op.History()
|
||||||
|
modified_target_faces = []
|
||||||
|
face_explorer = TopExp_Explorer(target.wrapped, ta.TopAbs_FACE)
|
||||||
|
while face_explorer.More():
|
||||||
|
target_face = TopoDS.Face_s(face_explorer.Current())
|
||||||
|
modified_los: TopTools_ListOfShape = history.Modified(target_face)
|
||||||
|
while not modified_los.IsEmpty():
|
||||||
|
modified_face = TopoDS.Face_s(modified_los.First())
|
||||||
|
modified_los.RemoveFirst()
|
||||||
|
modified_target_faces.append(modified_face)
|
||||||
|
face_explorer.Next()
|
||||||
|
|
||||||
|
# 3: Sew the resulting faces into shells - one for each surface the extrusion
|
||||||
|
# passes through and sort by distance from the profile
|
||||||
|
sewed_shape = _sew_topods_faces(modified_target_faces)
|
||||||
|
|
||||||
|
# From the sewed shape extract the shells and single faces
|
||||||
|
top_level_shapes = get_top_level_topods_shapes(sewed_shape)
|
||||||
|
modified_target_surfaces: ShapeList[Face | Shell] = ShapeList()
|
||||||
|
|
||||||
|
# For each of the top level Shells and Faces
|
||||||
|
for top_level_shape in top_level_shapes:
|
||||||
|
if isinstance(top_level_shape, TopoDS_Face):
|
||||||
|
modified_target_surfaces.append(Face(top_level_shape))
|
||||||
|
elif isinstance(top_level_shape, TopoDS_Shell):
|
||||||
|
modified_target_surfaces.append(Shell(top_level_shape))
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Invalid sewn shape {type(top_level_shape)}")
|
||||||
|
|
||||||
|
modified_target_surfaces = modified_target_surfaces.sort_by(
|
||||||
|
lambda s: s.distance_to(profile)
|
||||||
)
|
)
|
||||||
direction_axis = Axis(section.center(), clipping_direction)
|
limit = modified_target_surfaces[
|
||||||
# Create a linear extrusion to start
|
0 if until in [Until.NEXT, Until.PREVIOUS] else -1
|
||||||
extrusion = Solid.extrude(section, direction * max_dimension)
|
|
||||||
|
|
||||||
# Project section onto the shape to generate faces that will clip the extrusion
|
|
||||||
# and exclude the planar faces normal to the direction of extrusion and these
|
|
||||||
# will have no volume when extruded
|
|
||||||
faces = []
|
|
||||||
for face in section.project_to_shape(target_object, direction):
|
|
||||||
if isinstance(face, Face):
|
|
||||||
faces.append(face)
|
|
||||||
else:
|
|
||||||
faces += face.faces()
|
|
||||||
|
|
||||||
clip_faces = [
|
|
||||||
f
|
|
||||||
for f in faces
|
|
||||||
if not (f.is_planar and f.normal_at().dot(direction) == 0.0)
|
|
||||||
]
|
]
|
||||||
if not clip_faces:
|
keep: Literal[Keep.TOP, Keep.BOTTOM] = (
|
||||||
raise ValueError("provided face does not intersect target_object")
|
Keep.TOP if until in [Until.NEXT, Until.PREVIOUS] else Keep.BOTTOM
|
||||||
|
)
|
||||||
|
|
||||||
# Create the objects that will clip the linear extrusion
|
# 4: Split the extrusion by the appropriate shell
|
||||||
clipping_objects = [
|
clipped_extrusion = extrusion.split(limit, keep=keep)
|
||||||
Solid.extrude(f, clipping_direction).fix() for f in clip_faces
|
|
||||||
]
|
|
||||||
clipping_objects = [o for o in clipping_objects if o.volume > 1e-9]
|
|
||||||
|
|
||||||
if until == Until.NEXT:
|
# 5: Return the appropriate type
|
||||||
trimmed_extrusion = extrusion.cut(target_object)
|
if clipped_extrusion is None:
|
||||||
if isinstance(trimmed_extrusion, ShapeList):
|
raise RuntimeError("Extrusion is None") # None isn't an option here
|
||||||
closest_extrusion = trimmed_extrusion.sort_by(direction_axis)[0]
|
elif isinstance(clipped_extrusion, Solid):
|
||||||
else:
|
return clipped_extrusion
|
||||||
closest_extrusion = trimmed_extrusion
|
|
||||||
for clipping_object in clipping_objects:
|
|
||||||
# It's possible for clipping faces to self intersect when they are extruded
|
|
||||||
# thus they could be non manifold which results failed boolean operations
|
|
||||||
# - so skip these objects
|
|
||||||
try:
|
|
||||||
extrusion_shapes = closest_extrusion.cut(clipping_object)
|
|
||||||
except Exception:
|
|
||||||
warnings.warn(
|
|
||||||
"clipping error - extrusion may be incorrect",
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
base_part = extrusion.intersect(target_object)
|
# isinstance(clipped_extrusion, list):
|
||||||
if isinstance(base_part, ShapeList):
|
return ShapeList(clipped_extrusion).sort_by(
|
||||||
extrusion_parts = base_part
|
Axis(profile.center(), direction)
|
||||||
elif base_part is None:
|
)[0]
|
||||||
extrusion_parts = ShapeList()
|
|
||||||
else:
|
|
||||||
extrusion_parts = ShapeList([base_part])
|
|
||||||
for clipping_object in clipping_objects:
|
|
||||||
try:
|
|
||||||
clipped_extrusion = extrusion.intersect(clipping_object)
|
|
||||||
if clipped_extrusion is not None:
|
|
||||||
extrusion_parts.append(
|
|
||||||
clipped_extrusion.solids().sort_by(direction_axis)[0]
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
warnings.warn(
|
|
||||||
"clipping error - extrusion may be incorrect",
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
extrusion_shapes = Solid.fuse(*extrusion_parts)
|
|
||||||
|
|
||||||
result = extrusion_shapes.solids().sort_by(direction_axis)[0]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
def from_bounding_box(cls, bbox: BoundBox | OrientedBoundBox) -> Solid:
|
||||||
|
|
@ -982,12 +1150,14 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: Box
|
Solid: Box
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeBox(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeBox(
|
||||||
length,
|
plane.to_gp_ax2(),
|
||||||
width,
|
length,
|
||||||
height,
|
width,
|
||||||
).Shape()
|
height,
|
||||||
|
).Shape()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1014,13 +1184,15 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: Full or partial cone
|
Solid: Full or partial cone
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeCone(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeCone(
|
||||||
base_radius,
|
plane.to_gp_ax2(),
|
||||||
top_radius,
|
base_radius,
|
||||||
height,
|
top_radius,
|
||||||
angle * DEG2RAD,
|
height,
|
||||||
).Shape()
|
angle * DEG2RAD,
|
||||||
|
).Shape()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1045,12 +1217,14 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: Full or partial cylinder
|
Solid: Full or partial cylinder
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeCylinder(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeCylinder(
|
||||||
radius,
|
plane.to_gp_ax2(),
|
||||||
height,
|
radius,
|
||||||
angle * DEG2RAD,
|
height,
|
||||||
).Shape()
|
angle * DEG2RAD,
|
||||||
|
).Shape()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1071,7 +1245,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Returns:
|
Returns:
|
||||||
Solid: Lofted object
|
Solid: Lofted object
|
||||||
"""
|
"""
|
||||||
return cls(_make_loft(objs, True, ruled))
|
return cls(TopoDS.Solid_s(_make_loft(objs, True, ruled)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_sphere(
|
def make_sphere(
|
||||||
|
|
@ -1097,13 +1271,15 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: sphere
|
Solid: sphere
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeSphere(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeSphere(
|
||||||
radius,
|
plane.to_gp_ax2(),
|
||||||
angle1 * DEG2RAD,
|
radius,
|
||||||
angle2 * DEG2RAD,
|
angle1 * DEG2RAD,
|
||||||
angle3 * DEG2RAD,
|
angle2 * DEG2RAD,
|
||||||
).Shape()
|
angle3 * DEG2RAD,
|
||||||
|
).Shape()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1131,14 +1307,16 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: Full or partial torus
|
Solid: Full or partial torus
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeTorus(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeTorus(
|
||||||
major_radius,
|
plane.to_gp_ax2(),
|
||||||
minor_radius,
|
major_radius,
|
||||||
start_angle * DEG2RAD,
|
minor_radius,
|
||||||
end_angle * DEG2RAD,
|
start_angle * DEG2RAD,
|
||||||
major_angle * DEG2RAD,
|
end_angle * DEG2RAD,
|
||||||
).Shape()
|
major_angle * DEG2RAD,
|
||||||
|
).Shape()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1169,16 +1347,18 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
Solid: wedge
|
Solid: wedge
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
BRepPrimAPI_MakeWedge(
|
TopoDS.Solid_s(
|
||||||
plane.to_gp_ax2(),
|
BRepPrimAPI_MakeWedge(
|
||||||
delta_x,
|
plane.to_gp_ax2(),
|
||||||
delta_y,
|
delta_x,
|
||||||
delta_z,
|
delta_y,
|
||||||
min_x,
|
delta_z,
|
||||||
min_z,
|
min_x,
|
||||||
max_x,
|
min_z,
|
||||||
max_z,
|
max_x,
|
||||||
).Solid()
|
max_z,
|
||||||
|
).Solid()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -1216,7 +1396,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(revol_builder.Shape())
|
return cls(TopoDS.Solid_s(revol_builder.Shape()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sweep(
|
def sweep(
|
||||||
|
|
@ -1269,7 +1449,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
outer_wire = section
|
outer_wire = section
|
||||||
inner_wires = inner_wires if inner_wires else []
|
inner_wires = inner_wires if inner_wires else []
|
||||||
|
|
||||||
shapes = []
|
shapes: list[Mixin3D[TopoDS_Shape]] = []
|
||||||
for wire in [outer_wire] + inner_wires:
|
for wire in [outer_wire] + inner_wires:
|
||||||
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
|
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
|
||||||
|
|
||||||
|
|
@ -1364,7 +1544,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
if make_solid:
|
if make_solid:
|
||||||
builder.MakeSolid()
|
builder.MakeSolid()
|
||||||
|
|
||||||
return cls(builder.Shape())
|
return cls(TopoDS.Solid_s(builder.Shape()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def thicken(
|
def thicken(
|
||||||
|
|
@ -1420,7 +1600,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
)
|
)
|
||||||
offset_builder.MakeOffsetShape()
|
offset_builder.MakeOffsetShape()
|
||||||
try:
|
try:
|
||||||
result = Solid(offset_builder.Shape())
|
result = Solid(TopoDS.Solid_s(offset_builder.Shape()))
|
||||||
except StdFail_NotDone as err:
|
except StdFail_NotDone as err:
|
||||||
raise RuntimeError("Error applying thicken to given surface") from err
|
raise RuntimeError("Error applying thicken to given surface") from err
|
||||||
|
|
||||||
|
|
@ -1467,7 +1647,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
draft_angle_builder.Build()
|
draft_angle_builder.Build()
|
||||||
result = Solid(draft_angle_builder.Shape())
|
result = Solid(TopoDS.Solid_s(draft_angle_builder.Shape()))
|
||||||
except StdFail_NotDone as err:
|
except StdFail_NotDone as err:
|
||||||
raise DraftAngleError(
|
raise DraftAngleError(
|
||||||
"Draft build failed on the given solid.",
|
"Draft build failed on the given solid.",
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,15 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
|
from math import degrees
|
||||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||||
|
from typing import cast as tcast
|
||||||
|
|
||||||
import OCP.TopAbs as ta
|
import OCP.TopAbs as ta
|
||||||
from OCP.BRep import BRep_Builder, BRep_Tool
|
from OCP.BRep import BRep_Builder, BRep_Tool
|
||||||
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||||
from OCP.BRepAlgo import BRepAlgo
|
from OCP.BRepAlgo import BRepAlgo
|
||||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
|
||||||
from OCP.BRepBuilderAPI import (
|
from OCP.BRepBuilderAPI import (
|
||||||
BRepBuilderAPI_MakeEdge,
|
BRepBuilderAPI_MakeEdge,
|
||||||
BRepBuilderAPI_MakeFace,
|
BRepBuilderAPI_MakeFace,
|
||||||
|
|
@ -103,6 +105,7 @@ from OCP.Standard import (
|
||||||
Standard_ConstructionError,
|
Standard_ConstructionError,
|
||||||
Standard_Failure,
|
Standard_Failure,
|
||||||
Standard_NoSuchObject,
|
Standard_NoSuchObject,
|
||||||
|
Standard_TypeMismatch,
|
||||||
)
|
)
|
||||||
from OCP.StdFail import StdFail_NotDone
|
from OCP.StdFail import StdFail_NotDone
|
||||||
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt
|
from OCP.TColgp import TColgp_Array1OfPnt, TColgp_HArray2OfPnt
|
||||||
|
|
@ -139,10 +142,12 @@ from build123d.geometry import (
|
||||||
|
|
||||||
from .one_d import Edge, Mixin1D, Wire
|
from .one_d import Edge, Mixin1D, Wire
|
||||||
from .shape_core import (
|
from .shape_core import (
|
||||||
|
TOPODS,
|
||||||
Shape,
|
Shape,
|
||||||
ShapeList,
|
ShapeList,
|
||||||
SkipClean,
|
SkipClean,
|
||||||
_sew_topods_faces,
|
_sew_topods_faces,
|
||||||
|
_topods_bool_op,
|
||||||
_topods_entities,
|
_topods_entities,
|
||||||
_topods_face_normal_at,
|
_topods_face_normal_at,
|
||||||
downcast,
|
downcast,
|
||||||
|
|
@ -153,7 +158,6 @@ from .utils import (
|
||||||
_extrude_topods_shape,
|
_extrude_topods_shape,
|
||||||
_make_loft,
|
_make_loft,
|
||||||
_make_topods_face_from_wires,
|
_make_topods_face_from_wires,
|
||||||
_topods_bool_op,
|
|
||||||
find_max_dimension,
|
find_max_dimension,
|
||||||
)
|
)
|
||||||
from .zero_d import Vertex
|
from .zero_d import Vertex
|
||||||
|
|
@ -165,17 +169,11 @@ if TYPE_CHECKING: # pragma: no cover
|
||||||
T = TypeVar("T", Edge, Wire, "Face")
|
T = TypeVar("T", Edge, Wire, "Face")
|
||||||
|
|
||||||
|
|
||||||
class Mixin2D(ABC, Shape):
|
class Mixin2D(ABC, Shape[TOPODS]):
|
||||||
"""Additional methods to add to Face and Shell class"""
|
"""Additional methods to add to Face and Shell class"""
|
||||||
|
|
||||||
project_to_viewport = Mixin1D.project_to_viewport
|
# project_to_viewport = Mixin1D.project_to_viewport
|
||||||
split = Mixin1D.split
|
|
||||||
|
|
||||||
vertices = Mixin1D.vertices
|
|
||||||
vertex = Mixin1D.vertex
|
|
||||||
edges = Mixin1D.edges
|
|
||||||
edge = Mixin1D.edge
|
|
||||||
wires = Mixin1D.wires
|
|
||||||
# ---- Properties ----
|
# ---- Properties ----
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -213,23 +211,23 @@ class Mixin2D(ABC, Shape):
|
||||||
|
|
||||||
def __neg__(self) -> Self:
|
def __neg__(self) -> Self:
|
||||||
"""Reverse normal operator -"""
|
"""Reverse normal operator -"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Invalid Shape")
|
raise ValueError("Invalid Shape")
|
||||||
new_surface = copy.deepcopy(self)
|
new_surface = copy.deepcopy(self)
|
||||||
new_surface.wrapped = downcast(self.wrapped.Complemented())
|
new_surface.wrapped = tcast(TOPODS, downcast(self.wrapped.Complemented()))
|
||||||
|
|
||||||
# As the surface has been modified, the parent is no longer valid
|
# As the surface has been modified, the parent is no longer valid
|
||||||
new_surface.topo_parent = None
|
new_surface.topo_parent = None
|
||||||
|
|
||||||
return new_surface
|
return new_surface
|
||||||
|
|
||||||
def face(self) -> Face | None:
|
# def face(self) -> Face | None:
|
||||||
"""Return the Face"""
|
# """Return the Face"""
|
||||||
return Shape.get_single_shape(self, "Face")
|
# return Shape.get_single_shape(self, "Face")
|
||||||
|
|
||||||
def faces(self) -> ShapeList[Face]:
|
# def faces(self) -> ShapeList[Face]:
|
||||||
"""faces - all the faces in this Shape"""
|
# """faces - all the faces in this Shape"""
|
||||||
return Shape.get_shape_list(self, "Face")
|
# return Shape.get_shape_list(self, "Face")
|
||||||
|
|
||||||
def find_intersection_points(
|
def find_intersection_points(
|
||||||
self, other: Axis, tolerance: float = TOLERANCE
|
self, other: Axis, tolerance: float = TOLERANCE
|
||||||
|
|
@ -244,7 +242,7 @@ class Mixin2D(ABC, Shape):
|
||||||
Returns:
|
Returns:
|
||||||
list[tuple[Vector, Vector]]: Point and normal of intersection
|
list[tuple[Vector, Vector]]: Point and normal of intersection
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
intersection_line = gce_MakeLin(other.wrapped).Value()
|
intersection_line = gce_MakeLin(other.wrapped).Value()
|
||||||
|
|
@ -278,6 +276,128 @@ class Mixin2D(ABC, Shape):
|
||||||
|
|
||||||
return result
|
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 | BRepAlgoAPI_Common = (
|
||||||
|
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
|
@abstractmethod
|
||||||
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
def location_at(self, *args: Any, **kwargs: Any) -> Location:
|
||||||
"""A location from a face or shell"""
|
"""A location from a face or shell"""
|
||||||
|
|
@ -287,13 +407,32 @@ class Mixin2D(ABC, Shape):
|
||||||
"""Return a copy of self moved along the normal by amount"""
|
"""Return a copy of self moved along the normal by amount"""
|
||||||
return copy.deepcopy(self).moved(Location(self.normal_at() * amount))
|
return copy.deepcopy(self).moved(Location(self.normal_at() * amount))
|
||||||
|
|
||||||
def shell(self) -> Shell | None:
|
def project_to_viewport(
|
||||||
"""Return the Shell"""
|
self,
|
||||||
return Shape.get_single_shape(self, "Shell")
|
viewport_origin: VectorLike,
|
||||||
|
viewport_up: VectorLike = (0, 0, 1),
|
||||||
|
look_at: VectorLike | None = None,
|
||||||
|
focus: float | None = None,
|
||||||
|
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
|
||||||
|
"""project_to_viewport
|
||||||
|
|
||||||
def shells(self) -> ShapeList[Shell]:
|
Project a shape onto a viewport returning visible and hidden Edges.
|
||||||
"""shells - all the shells in this Shape"""
|
|
||||||
return Shape.get_shape_list(self, "Shell")
|
Args:
|
||||||
|
viewport_origin (VectorLike): location of viewport
|
||||||
|
viewport_up (VectorLike, optional): direction of the viewport y axis.
|
||||||
|
Defaults to (0, 0, 1).
|
||||||
|
look_at (VectorLike, optional): point to look at.
|
||||||
|
Defaults to None (center of shape).
|
||||||
|
focus (float, optional): the focal length for perspective projection
|
||||||
|
Defaults to None (orthographic projection)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[ShapeList[Edge],ShapeList[Edge]]: visible & hidden Edges
|
||||||
|
"""
|
||||||
|
return Mixin1D.project_to_viewport(
|
||||||
|
self, viewport_origin, viewport_up, look_at, focus
|
||||||
|
)
|
||||||
|
|
||||||
def _wrap_edge(
|
def _wrap_edge(
|
||||||
self,
|
self,
|
||||||
|
|
@ -350,7 +489,7 @@ class Mixin2D(ABC, Shape):
|
||||||
world_point, world_point - target_object_center
|
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")
|
raise ValueError("Can't wrap around an empty face")
|
||||||
|
|
||||||
# Initial setup
|
# Initial setup
|
||||||
|
|
@ -411,7 +550,7 @@ class Mixin2D(ABC, Shape):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
|
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")
|
raise RuntimeError("Wrapped edge is invalid")
|
||||||
|
|
||||||
if not snap_to_face:
|
if not snap_to_face:
|
||||||
|
|
@ -434,7 +573,7 @@ class Mixin2D(ABC, Shape):
|
||||||
return projected_edge
|
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
|
"""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.
|
structure. It encapsulates geometric information, defining a face of a 3D shape.
|
||||||
These faces are integral components of complex structures, such as solids and
|
These faces are integral components of complex structures, such as solids and
|
||||||
|
|
@ -449,7 +588,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
@overload
|
@overload
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
obj: TopoDS_Face,
|
obj: TopoDS_Face | Plane,
|
||||||
label: str = "",
|
label: str = "",
|
||||||
color: Color | None = None,
|
color: Color | None = None,
|
||||||
parent: Compound | None = None,
|
parent: Compound | None = None,
|
||||||
|
|
@ -457,7 +596,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
|
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (TopoDS_Shape, optional): OCCT Face.
|
obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
|
||||||
label (str, optional): Defaults to ''.
|
label (str, optional): Defaults to ''.
|
||||||
color (Color, optional): Defaults to None.
|
color (Color, optional): Defaults to None.
|
||||||
parent (Compound, optional): assembly parent. Defaults to None.
|
parent (Compound, optional): assembly parent. Defaults to None.
|
||||||
|
|
@ -483,11 +622,14 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any):
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
|
obj: TopoDS_Face | Plane | None
|
||||||
outer_wire, inner_wires, obj, label, color, parent = (None,) * 6
|
outer_wire, inner_wires, obj, label, color, parent = (None,) * 6
|
||||||
|
|
||||||
if args:
|
if args:
|
||||||
l_a = len(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)
|
obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
|
||||||
elif isinstance(args[0], Wire):
|
elif isinstance(args[0], Wire):
|
||||||
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
|
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
|
||||||
|
|
@ -516,6 +658,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
color = kwargs.get("color", color)
|
color = kwargs.get("color", color)
|
||||||
parent = kwargs.get("parent", parent)
|
parent = kwargs.get("parent", parent)
|
||||||
|
|
||||||
|
if isinstance(obj, Plane):
|
||||||
|
obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
|
||||||
|
|
||||||
if outer_wire is not None:
|
if outer_wire is not None:
|
||||||
inner_topods_wires = (
|
inner_topods_wires = (
|
||||||
[w.wrapped for w in inner_wires] if inner_wires is not None else []
|
[w.wrapped for w in inner_wires] if inner_wires is not None else []
|
||||||
|
|
@ -545,7 +690,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
float: The total surface area, including the area of holes. Returns 0.0 if
|
float: The total surface area, including the area of holes. Returns 0.0 if
|
||||||
the face is empty.
|
the face is empty.
|
||||||
"""
|
"""
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
return self.without_holes().area
|
return self.without_holes().area
|
||||||
|
|
@ -556,6 +701,11 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface:
|
if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self.geom_type == GeomType.CONE:
|
||||||
|
return Axis(
|
||||||
|
self.geom_adaptor().Cone().Axis() # type:ignore[attr-defined]
|
||||||
|
)
|
||||||
|
|
||||||
if self.geom_type == GeomType.CYLINDER:
|
if self.geom_type == GeomType.CYLINDER:
|
||||||
return Axis(
|
return Axis(
|
||||||
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
|
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
|
||||||
|
|
@ -605,7 +755,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
ValueError: If the face or its underlying representation is empty.
|
ValueError: If the face or its underlying representation is empty.
|
||||||
ValueError: If the face is not planar.
|
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")
|
raise ValueError("Can't determine axes_of_symmetry of empty face")
|
||||||
|
|
||||||
if not self.is_planar_face:
|
if not self.is_planar_face:
|
||||||
|
|
@ -671,15 +821,13 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
).sort_by(Axis(cog, cross_dir))
|
).sort_by(Axis(cog, cross_dir))
|
||||||
|
|
||||||
bottom_area = sum(f.area for f in bottom_list)
|
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):
|
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
|
||||||
intersection = flipped_face.intersect(bottom_face)
|
intersection = flipped_face.intersect(bottom_face)
|
||||||
if intersection is None or isinstance(intersection, list):
|
if intersection is None:
|
||||||
intersect_area = -1.0
|
intersect_area = -1.0
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
assert isinstance(intersection, Face)
|
intersect_area = sum(f.area for f in intersection.faces())
|
||||||
intersect_area += intersection.area
|
|
||||||
|
|
||||||
if intersect_area == -1.0:
|
if intersect_area == -1.0:
|
||||||
continue
|
continue
|
||||||
|
|
@ -837,6 +985,17 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def semi_angle(self) -> None | float:
|
||||||
|
"""Return the semi angle of a cone, otherwise None"""
|
||||||
|
if (
|
||||||
|
self.geom_type == GeomType.CONE
|
||||||
|
and type(self.geom_adaptor()) != Geom_RectangularTrimmedSurface
|
||||||
|
):
|
||||||
|
return degrees(self.geom_adaptor().SemiAngle()) # type:ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume(self) -> float:
|
def volume(self) -> float:
|
||||||
"""volume - the volume of this Face, which is always zero"""
|
"""volume - the volume of this Face, which is always zero"""
|
||||||
|
|
@ -871,7 +1030,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
Returns:
|
Returns:
|
||||||
Face: extruded shape
|
Face: extruded shape
|
||||||
"""
|
"""
|
||||||
if obj.wrapped is None:
|
if not obj:
|
||||||
raise ValueError("Can't extrude empty object")
|
raise ValueError("Can't extrude empty object")
|
||||||
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
|
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
|
||||||
|
|
||||||
|
|
@ -981,7 +1140,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
)
|
)
|
||||||
return single_point_curve
|
return single_point_curve
|
||||||
|
|
||||||
if shape.wrapped is None:
|
if not shape:
|
||||||
raise ValueError("input Edge cannot be empty")
|
raise ValueError("input Edge cannot be empty")
|
||||||
|
|
||||||
adaptor = BRepAdaptor_Curve(shape.wrapped)
|
adaptor = BRepAdaptor_Curve(shape.wrapped)
|
||||||
|
|
@ -1015,6 +1174,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
plane: Plane = Plane.XY,
|
plane: Plane = Plane.XY,
|
||||||
) -> Face:
|
) -> Face:
|
||||||
"""Create a unlimited size Face aligned with plane"""
|
"""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()
|
pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
|
||||||
return cls(pln_shape)
|
return cls(pln_shape)
|
||||||
|
|
||||||
|
|
@ -1104,7 +1269,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
raise ValueError("exterior must be a Wire or list of Edges")
|
raise ValueError("exterior must be a Wire or list of Edges")
|
||||||
|
|
||||||
for edge in outside_edges:
|
for edge in outside_edges:
|
||||||
if edge.wrapped is None:
|
if not edge:
|
||||||
raise ValueError("exterior contains empty edges")
|
raise ValueError("exterior contains empty edges")
|
||||||
surface.Add(edge.wrapped, GeomAbs_C0)
|
surface.Add(edge.wrapped, GeomAbs_C0)
|
||||||
|
|
||||||
|
|
@ -1135,7 +1300,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
if interior_wires:
|
if interior_wires:
|
||||||
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
|
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
|
||||||
for wire in interior_wires:
|
for wire in interior_wires:
|
||||||
if wire.wrapped is None:
|
if not wire:
|
||||||
raise ValueError("interior_wires contain an empty wire")
|
raise ValueError("interior_wires contain an empty wire")
|
||||||
makeface_object.Add(wire.wrapped)
|
makeface_object.Add(wire.wrapped)
|
||||||
try:
|
try:
|
||||||
|
|
@ -1317,7 +1482,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
patch.Build()
|
patch.Build()
|
||||||
result = cls(patch.Shape())
|
result = cls(TopoDS.Face_s(patch.Shape()))
|
||||||
except (
|
except (
|
||||||
Standard_Failure,
|
Standard_Failure,
|
||||||
StdFail_NotDone,
|
StdFail_NotDone,
|
||||||
|
|
@ -1329,7 +1494,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
result = result.fix()
|
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")
|
raise RuntimeError("Non planar face is invalid")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -1433,8 +1598,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
|
|
||||||
if len(profile.edges()) != 1 or len(path.edges()) != 1:
|
if len(profile.edges()) != 1 or len(path.edges()) != 1:
|
||||||
raise ValueError("Use Shell.sweep for multi Edge objects")
|
raise ValueError("Use Shell.sweep for multi Edge objects")
|
||||||
profile = Wire([profile.edge()])
|
profile_edge = profile.edge()
|
||||||
path = Wire([path.edge()])
|
path_edge = path.edge()
|
||||||
|
assert profile_edge is not None
|
||||||
|
assert path_edge is not None
|
||||||
|
profile = Wire([profile_edge])
|
||||||
|
path = Wire([path_edge])
|
||||||
builder = BRepOffsetAPI_MakePipeShell(path.wrapped)
|
builder = BRepOffsetAPI_MakePipeShell(path.wrapped)
|
||||||
builder.Add(profile.wrapped, False, False)
|
builder.Add(profile.wrapped, False, False)
|
||||||
builder.SetTransitionMode(Shape._transModeDict[transition])
|
builder.SetTransitionMode(Shape._transModeDict[transition])
|
||||||
|
|
@ -1458,6 +1627,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
Returns:
|
Returns:
|
||||||
Vector: center
|
Vector: center
|
||||||
"""
|
"""
|
||||||
|
center_point: Vector | gp_Pnt
|
||||||
if (center_of == CenterOf.MASS) or (
|
if (center_of == CenterOf.MASS) or (
|
||||||
center_of == CenterOf.GEOMETRY and self.is_planar
|
center_of == CenterOf.GEOMETRY and self.is_planar
|
||||||
):
|
):
|
||||||
|
|
@ -1517,7 +1687,10 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
|
|
||||||
# Index or iterator access to OCP.TopTools.TopTools_ListOfShape is slow on M1 macs
|
# Index or iterator access to OCP.TopTools.TopTools_ListOfShape is slow on M1 macs
|
||||||
# Using First() and Last() to omit
|
# Using First() and Last() to omit
|
||||||
edges = (Edge(edge_list.First()), Edge(edge_list.Last()))
|
edges = (
|
||||||
|
Edge(TopoDS.Edge_s(edge_list.First())),
|
||||||
|
Edge(TopoDS.Edge_s(edge_list.Last())),
|
||||||
|
)
|
||||||
|
|
||||||
edge1, edge2 = Wire.order_chamfer_edges(reference_edge, edges)
|
edge1, edge2 = Wire.order_chamfer_edges(reference_edge, edges)
|
||||||
|
|
||||||
|
|
@ -1907,7 +2080,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
BRepAlgoAPI_Common(),
|
BRepAlgoAPI_Common(),
|
||||||
)
|
)
|
||||||
for topods_shell in get_top_level_topods_shapes(topods_shape):
|
for topods_shell in get_top_level_topods_shapes(topods_shape):
|
||||||
intersected_shapes.append(Shell(topods_shell))
|
intersected_shapes.append(Shell(TopoDS.Shell_s(topods_shell)))
|
||||||
|
|
||||||
intersected_shapes = intersected_shapes.sort_by(Axis(self.center(), direction))
|
intersected_shapes = intersected_shapes.sort_by(Axis(self.center(), direction))
|
||||||
projected_shapes: ShapeList[Face | Shell] = ShapeList()
|
projected_shapes: ShapeList[Face | Shell] = ShapeList()
|
||||||
|
|
@ -1940,7 +2113,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
if self.wrapped is None:
|
if self._wrapped is None:
|
||||||
raise ValueError("Cannot approximate an empty shape")
|
raise ValueError("Cannot approximate an empty shape")
|
||||||
|
|
||||||
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
|
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
|
||||||
|
|
@ -1953,7 +2126,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
Returns:
|
Returns:
|
||||||
Face: A new Face instance identical to the original but without any holes.
|
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")
|
raise ValueError("Cannot remove holes from an empty face")
|
||||||
|
|
||||||
if not (inner_wires := self.inner_wires()):
|
if not (inner_wires := self.inner_wires()):
|
||||||
|
|
@ -1964,7 +2137,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
for hole_wire in inner_wires:
|
for hole_wire in inner_wires:
|
||||||
reshaper.Remove(hole_wire.wrapped)
|
reshaper.Remove(hole_wire.wrapped)
|
||||||
modified_shape = downcast(reshaper.Apply(self.wrapped))
|
modified_shape = downcast(reshaper.Apply(self.wrapped))
|
||||||
holeless.wrapped = modified_shape
|
holeless.wrapped = TopoDS.Face_s(modified_shape)
|
||||||
return holeless
|
return holeless
|
||||||
|
|
||||||
def wire(self) -> Wire:
|
def wire(self) -> Wire:
|
||||||
|
|
@ -2327,7 +2500,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
|
||||||
return wrapped_wire
|
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
|
"""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
|
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
|
part of a geometric model, it defines a watertight enclosure, commonly encountered
|
||||||
|
|
@ -2359,7 +2532,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
obj = obj_list[0]
|
obj = obj_list[0]
|
||||||
|
|
||||||
if isinstance(obj, Face):
|
if isinstance(obj, Face):
|
||||||
if obj.wrapped is None:
|
if not obj:
|
||||||
raise ValueError(f"Can't create a Shell from empty Face")
|
raise ValueError(f"Can't create a Shell from empty Face")
|
||||||
builder = BRep_Builder()
|
builder = BRep_Builder()
|
||||||
shell = TopoDS_Shell()
|
shell = TopoDS_Shell()
|
||||||
|
|
@ -2367,7 +2540,10 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
builder.Add(shell, obj.wrapped)
|
builder.Add(shell, obj.wrapped)
|
||||||
obj = shell
|
obj = shell
|
||||||
elif isinstance(obj, Iterable):
|
elif isinstance(obj, Iterable):
|
||||||
obj = _sew_topods_faces([f.wrapped for f in obj])
|
try:
|
||||||
|
obj = TopoDS.Shell_s(_sew_topods_faces([f.wrapped for f in obj]))
|
||||||
|
except Standard_TypeMismatch:
|
||||||
|
raise TypeError("Unable to create Shell, invalid input type")
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
obj=obj,
|
obj=obj,
|
||||||
|
|
@ -2385,6 +2561,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
solid_shell = ShapeFix_Solid().SolidFromShell(self.wrapped)
|
solid_shell = ShapeFix_Solid().SolidFromShell(self.wrapped)
|
||||||
properties = GProp_GProps()
|
properties = GProp_GProps()
|
||||||
calc_function = Shape.shape_properties_LUT[shapetype(solid_shell)]
|
calc_function = Shape.shape_properties_LUT[shapetype(solid_shell)]
|
||||||
|
assert calc_function is not None
|
||||||
calc_function(solid_shell, properties)
|
calc_function(solid_shell, properties)
|
||||||
return properties.Mass()
|
return properties.Mass()
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
@ -2427,7 +2604,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
Returns:
|
Returns:
|
||||||
Shell: Lofted object
|
Shell: Lofted object
|
||||||
"""
|
"""
|
||||||
return cls(_make_loft(objs, False, ruled))
|
return cls(TopoDS.Shell_s(_make_loft(objs, False, ruled)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revolve(
|
def revolve(
|
||||||
|
|
@ -2453,7 +2630,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
profile.wrapped, axis.wrapped, angle * DEG2RAD, True
|
profile.wrapped, axis.wrapped, angle * DEG2RAD, True
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(revol_builder.Shape())
|
return cls(TopoDS.Shell_s(revol_builder.Shape()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sweep(
|
def sweep(
|
||||||
|
|
@ -2481,7 +2658,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
|
||||||
builder.Add(profile.wrapped, False, False)
|
builder.Add(profile.wrapped, False, False)
|
||||||
builder.SetTransitionMode(Shape._transModeDict[transition])
|
builder.SetTransitionMode(Shape._transModeDict[transition])
|
||||||
builder.Build()
|
builder.Build()
|
||||||
result = Shell(builder.Shape())
|
result = Shell(TopoDS.Shell_s(builder.Shape()))
|
||||||
if SkipClean.clean:
|
if SkipClean.clean:
|
||||||
result = result.clean()
|
result = result.clean()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ Key Features:
|
||||||
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
- `_make_topods_face_from_wires`: Generates planar faces with optional holes.
|
||||||
|
|
||||||
- **Boolean Operations**:
|
- **Boolean Operations**:
|
||||||
- `_topods_bool_op`: Generic Boolean operations for TopoDS_Shapes.
|
|
||||||
- `new_edges`: Identifies newly created edges from combined shapes.
|
- `new_edges`: Identifies newly created edges from combined shapes.
|
||||||
|
|
||||||
- **Enhanced Math**:
|
- **Enhanced Math**:
|
||||||
|
|
@ -282,45 +281,6 @@ def _make_topods_face_from_wires(
|
||||||
return TopoDS.Face_s(sf_f.Result())
|
return TopoDS.Face_s(sf_f.Result())
|
||||||
|
|
||||||
|
|
||||||
def _topods_bool_op(
|
|
||||||
args: Iterable[TopoDS_Shape],
|
|
||||||
tools: Iterable[TopoDS_Shape],
|
|
||||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter,
|
|
||||||
) -> TopoDS_Shape:
|
|
||||||
"""Generic boolean operation for TopoDS_Shapes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
args: Iterable[TopoDS_Shape]:
|
|
||||||
tools: Iterable[TopoDS_Shape]:
|
|
||||||
operation: BRepAlgoAPI_BooleanOperation | BRepAlgoAPI_Splitter:
|
|
||||||
|
|
||||||
Returns: TopoDS_Shape
|
|
||||||
|
|
||||||
"""
|
|
||||||
args = list(args)
|
|
||||||
tools = list(tools)
|
|
||||||
arg = TopTools_ListOfShape()
|
|
||||||
for obj in args:
|
|
||||||
arg.Append(obj)
|
|
||||||
|
|
||||||
tool = TopTools_ListOfShape()
|
|
||||||
for obj in tools:
|
|
||||||
tool.Append(obj)
|
|
||||||
|
|
||||||
operation.SetArguments(arg)
|
|
||||||
operation.SetTools(tool)
|
|
||||||
|
|
||||||
operation.SetRunParallel(True)
|
|
||||||
operation.Build()
|
|
||||||
|
|
||||||
result = downcast(operation.Shape())
|
|
||||||
# Remove unnecessary TopoDS_Compound around single shape
|
|
||||||
if isinstance(result, TopoDS_Compound):
|
|
||||||
result = unwrap_topods_compound(result, True)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]:
|
def delta(shapes_one: Iterable[Shape], shapes_two: Iterable[Shape]) -> list[Shape]:
|
||||||
"""Compare the OCCT objects of each list and return the differences"""
|
"""Compare the OCCT objects of each list and return the differences"""
|
||||||
shapes_one = list(shapes_one)
|
shapes_one = list(shapes_one)
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ from OCP.TopExp import TopExp_Explorer
|
||||||
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
|
from OCP.TopoDS import TopoDS, TopoDS_Shape, TopoDS_Vertex, TopoDS_Edge
|
||||||
from OCP.gp import gp_Pnt
|
from OCP.gp import gp_Pnt
|
||||||
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
from build123d.geometry import Matrix, Vector, VectorLike, Location, Axis, Plane
|
||||||
|
from build123d.build_enums import Keep
|
||||||
from .shape_core import Shape, ShapeList, downcast, shapetype
|
from .shape_core import Shape, ShapeList, TrimmingTool, downcast, shapetype
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
|
@ -161,7 +161,7 @@ class Vertex(Shape[TopoDS_Vertex]):
|
||||||
|
|
||||||
shape_type = shapetype(obj)
|
shape_type = shapetype(obj)
|
||||||
# NB downcast is needed to handle TopoDS_Shape types
|
# NB downcast is needed to handle TopoDS_Shape types
|
||||||
return constructor_lut[shape_type](downcast(obj))
|
return constructor_lut[shape_type](TopoDS.Vertex_s(obj))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
def extrude(cls, obj: Shape, direction: VectorLike) -> Vertex:
|
||||||
|
|
@ -312,6 +312,10 @@ class Vertex(Shape[TopoDS_Vertex]):
|
||||||
"""The center of a vertex is itself!"""
|
"""The center of a vertex is itself!"""
|
||||||
return Vector(self)
|
return Vector(self)
|
||||||
|
|
||||||
|
def split(self, tool: TrimmingTool, keep: Keep = Keep.TOP):
|
||||||
|
"""split - not implemented"""
|
||||||
|
raise NotImplementedError("Vertices cannot be split.")
|
||||||
|
|
||||||
def to_tuple(self) -> tuple[float, float, float]:
|
def to_tuple(self) -> tuple[float, float, float]:
|
||||||
"""Return vertex as three tuple of floats"""
|
"""Return vertex as three tuple of floats"""
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ def to_vtk_poly_data(
|
||||||
if not HAS_VTK:
|
if not HAS_VTK:
|
||||||
warnings.warn("VTK not supported", stacklevel=2)
|
warnings.warn("VTK not supported", stacklevel=2)
|
||||||
|
|
||||||
if obj.wrapped is None:
|
if not obj:
|
||||||
raise ValueError("Cannot convert an empty shape")
|
raise ValueError("Cannot convert an empty shape")
|
||||||
|
|
||||||
vtk_shape = IVtkOCC_Shape(obj.wrapped)
|
vtk_shape = IVtkOCC_Shape(obj.wrapped)
|
||||||
|
|
|
||||||
|
|
@ -98,14 +98,14 @@ class BuildLineTests(unittest.TestCase):
|
||||||
powerup @ 0,
|
powerup @ 0,
|
||||||
tangents=(screw % 1, powerup % 0),
|
tangents=(screw % 1, powerup % 0),
|
||||||
)
|
)
|
||||||
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.983628932414, 5)
|
self.assertAlmostEqual(roller_coaster.wires()[0].length, 678.9785865257071, 5)
|
||||||
|
|
||||||
def test_bezier(self):
|
def test_bezier(self):
|
||||||
pts = [(0, 0), (20, 20), (40, 0), (0, -40), (-60, 0), (0, 100), (100, 0)]
|
pts = [(0, 0), (20, 20), (40, 0), (0, -40), (-60, 0), (0, 100), (100, 0)]
|
||||||
wts = [1.0, 1.0, 2.0, 3.0, 4.0, 2.0, 1.0]
|
wts = [1.0, 1.0, 2.0, 3.0, 4.0, 2.0, 1.0]
|
||||||
with BuildLine() as bz:
|
with BuildLine() as bz:
|
||||||
b1 = Bezier(*pts, weights=wts)
|
b1 = Bezier(*pts, weights=wts)
|
||||||
self.assertAlmostEqual(bz.wires()[0].length, 225.86389406824566, 5)
|
self.assertAlmostEqual(bz.wires()[0].length, 225.98661946375782, 5)
|
||||||
self.assertTrue(isinstance(b1, Edge))
|
self.assertTrue(isinstance(b1, Edge))
|
||||||
|
|
||||||
def test_double_tangent_arc(self):
|
def test_double_tangent_arc(self):
|
||||||
|
|
@ -183,6 +183,61 @@ class BuildLineTests(unittest.TestCase):
|
||||||
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
||||||
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
|
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
|
||||||
|
|
||||||
|
with BuildLine(Plane.YZ):
|
||||||
|
p = FilletPolyline(
|
||||||
|
(0, 0),
|
||||||
|
(10, 0),
|
||||||
|
(10, 10),
|
||||||
|
(0, 10),
|
||||||
|
radius=(1, 2, 3, 0),
|
||||||
|
close=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 3)
|
||||||
|
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
|
||||||
|
|
||||||
|
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):
|
with BuildLine(Plane.YZ):
|
||||||
p = FilletPolyline(
|
p = FilletPolyline(
|
||||||
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
|
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
|
||||||
|
|
@ -197,6 +252,33 @@ class BuildLineTests(unittest.TestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
|
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
|
||||||
|
|
||||||
|
# test filletpolyline curr_fillet None
|
||||||
|
# Middle corner radius = 0 → curr_fillet is None
|
||||||
|
with BuildLine():
|
||||||
|
p = FilletPolyline(
|
||||||
|
(0, 0),
|
||||||
|
(10, 0),
|
||||||
|
(10, 10),
|
||||||
|
(20, 10),
|
||||||
|
radius=(0, 1), # middle corner is sharp
|
||||||
|
close=False,
|
||||||
|
)
|
||||||
|
# 1 circular fillet, 3 line fillets
|
||||||
|
assert len(p.edges().filter_by(GeomType.CIRCLE)) == 1
|
||||||
|
|
||||||
|
# test filletpolyline next_fillet None:
|
||||||
|
# Second corner is sharp (radius 0) → next_fillet is None
|
||||||
|
with BuildLine():
|
||||||
|
p = FilletPolyline(
|
||||||
|
(0, 0),
|
||||||
|
(10, 0),
|
||||||
|
(10, 10),
|
||||||
|
(0, 10),
|
||||||
|
radius=(1, 0), # next_fillet is None at last interior corner
|
||||||
|
close=False,
|
||||||
|
)
|
||||||
|
assert len(p.edges()) > 0
|
||||||
|
|
||||||
def test_intersecting_line(self):
|
def test_intersecting_line(self):
|
||||||
with BuildLine():
|
with BuildLine():
|
||||||
l1 = Line((0, 0), (10, 0))
|
l1 = Line((0, 0), (10, 0))
|
||||||
|
|
@ -805,9 +887,9 @@ class BuildLineTests(unittest.TestCase):
|
||||||
min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2
|
min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2
|
||||||
max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2
|
max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2
|
||||||
|
|
||||||
print(case[1], min_r, max_r, case[0])
|
# print(case[1], min_r, max_r, case[0])
|
||||||
print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01)
|
# print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01)
|
||||||
print((case[0] - 1 * (r1 + r2)) / 2)
|
# print((case[0] - 1 * (r1 + r2)) / 2)
|
||||||
|
|
||||||
# Greater than min
|
# Greater than min
|
||||||
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])
|
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,60 @@ class TestExtrude(unittest.TestCase):
|
||||||
extrude(until=Until.NEXT)
|
extrude(until=Until.NEXT)
|
||||||
self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5)
|
self.assertAlmostEqual(test.part.volume, 10**3 - 8**3 + 1**2 * 8, 5)
|
||||||
|
|
||||||
|
def test_extrude_until2(self):
|
||||||
|
target = Box(10, 5, 5) - Pos(X=2.5) * Cylinder(0.5, 5)
|
||||||
|
pln = Plane((7, 0, 7), z_dir=(-1, 0, -1))
|
||||||
|
profile = (pln * Circle(1)).face()
|
||||||
|
extrusion = extrude(profile, dir=pln.z_dir, until=Until.NEXT, target=target)
|
||||||
|
self.assertLess(extrusion.bounding_box().min.Z, 2.5)
|
||||||
|
|
||||||
|
def test_extrude_until3(self):
|
||||||
|
with BuildPart() as p:
|
||||||
|
with BuildSketch(Plane.XZ):
|
||||||
|
Rectangle(8, 8, align=Align.MIN)
|
||||||
|
with Locations((1, 1)):
|
||||||
|
Rectangle(7, 7, align=Align.MIN, mode=Mode.SUBTRACT)
|
||||||
|
extrude(amount=2, both=True)
|
||||||
|
with BuildSketch(
|
||||||
|
Plane((-2, 0, -2), x_dir=(0, 1, 0), z_dir=(1, 0, 1))
|
||||||
|
) as profile:
|
||||||
|
Rectangle(4, 1)
|
||||||
|
extrude(until=Until.NEXT)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(p.part.volume, 72.313, 2)
|
||||||
|
|
||||||
|
def test_extrude_until_errors(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
extrude(
|
||||||
|
Rectangle(1, 1),
|
||||||
|
until=Until.NEXT,
|
||||||
|
dir=(0, 0, 1),
|
||||||
|
target=Pos(Z=-10) * Box(1, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_extrude_until_invalid_sewn_shape(self):
|
||||||
|
profile = Face.make_rect(1, 1)
|
||||||
|
target = Box(2, 2, 2)
|
||||||
|
direction = Vector(0, 0, 1)
|
||||||
|
|
||||||
|
bad_shape = Box(1, 1, 1).wrapped # not a Face or Shell → forces RuntimeError
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"build123d.topology.three_d.get_top_level_topods_shapes",
|
||||||
|
return_value=[bad_shape],
|
||||||
|
):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
extrude(profile, dir=direction, until=Until.NEXT, target=target)
|
||||||
|
|
||||||
|
def test_extrude_until_invalid_split(self):
|
||||||
|
profile = Face.make_rect(1, 1)
|
||||||
|
target = Box(2, 2, 2)
|
||||||
|
direction = Vector(0, 0, 1)
|
||||||
|
|
||||||
|
with patch("build123d.topology.three_d.Solid.split", return_value=None):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
extrude(profile, dir=direction, until=Until.NEXT, target=target)
|
||||||
|
|
||||||
def test_extrude_face(self):
|
def test_extrude_face(self):
|
||||||
with BuildPart(Plane.XZ) as box:
|
with BuildPart(Plane.XZ) as box:
|
||||||
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
with BuildSketch(Plane.XZ, mode=Mode.PRIVATE) as square:
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class TestBoundBox(unittest.TestCase):
|
||||||
|
|
||||||
# OCC uses some approximations
|
# OCC uses some approximations
|
||||||
self.assertAlmostEqual(bb1.size.X, 1.0, 1)
|
self.assertAlmostEqual(bb1.size.X, 1.0, 1)
|
||||||
|
self.assertAlmostEqual(bb1.measure, 1.0, 5)
|
||||||
|
|
||||||
# Test adding to an existing bounding box
|
# Test adding to an existing bounding box
|
||||||
v0 = Vertex(0, 0, 0)
|
v0 = Vertex(0, 0, 0)
|
||||||
|
|
@ -50,6 +51,7 @@ class TestBoundBox(unittest.TestCase):
|
||||||
|
|
||||||
bb3 = bb1.add(bb2)
|
bb3 = bb1.add(bb2)
|
||||||
self.assertAlmostEqual(bb3.size, (2, 2, 2), 7)
|
self.assertAlmostEqual(bb3.size, (2, 2, 2), 7)
|
||||||
|
self.assertAlmostEqual(bb3.measure, 8, 5)
|
||||||
|
|
||||||
bb3 = bb2.add((3, 3, 3))
|
bb3 = bb2.add((3, 3, 3))
|
||||||
self.assertAlmostEqual(bb3.size, (3, 3, 3), 7)
|
self.assertAlmostEqual(bb3.size, (3, 3, 3), 7)
|
||||||
|
|
@ -61,6 +63,7 @@ class TestBoundBox(unittest.TestCase):
|
||||||
bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box())
|
bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box())
|
||||||
bb2 = Vertex(0, 0, 0).bounding_box().add(Vertex(3, 3, 0).bounding_box())
|
bb2 = Vertex(0, 0, 0).bounding_box().add(Vertex(3, 3, 0).bounding_box())
|
||||||
bb3 = Vertex(0, 0, 0).bounding_box().add(Vertex(1.5, 1.5, 0).bounding_box())
|
bb3 = Vertex(0, 0, 0).bounding_box().add(Vertex(1.5, 1.5, 0).bounding_box())
|
||||||
|
self.assertAlmostEqual(bb2.measure, 9, 5)
|
||||||
# Test that bb2 contains bb1
|
# Test that bb2 contains bb1
|
||||||
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2))
|
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2))
|
||||||
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))
|
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))
|
||||||
|
|
|
||||||
|
|
@ -26,169 +26,296 @@ license:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import colorsys
|
||||||
import copy
|
import copy
|
||||||
import unittest
|
import math
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from build123d.geometry import Color
|
import pytest
|
||||||
|
|
||||||
from OCP.Quantity import Quantity_ColorRGBA
|
from OCP.Quantity import Quantity_ColorRGBA
|
||||||
|
from build123d.geometry import Color
|
||||||
|
|
||||||
|
|
||||||
class TestColor(unittest.TestCase):
|
# Overloads
|
||||||
# name + alpha overload
|
@pytest.mark.parametrize(
|
||||||
def test_name1(self):
|
"color, expected",
|
||||||
c = Color("blue")
|
[
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
|
pytest.param(Color("blue"), (0, 0, 1, 1), id="name"),
|
||||||
|
pytest.param(Color("blue", alpha=0.5), (0, 0, 1, 0.5), id="name + kw alpha"),
|
||||||
|
pytest.param(Color("blue", 0.5), (0, 0, 1, 0.5), id="name + alpha"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overload_name(color, expected):
|
||||||
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
def test_name2(self):
|
|
||||||
c = Color("blue", alpha=0.5)
|
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
|
|
||||||
|
|
||||||
def test_name3(self):
|
@pytest.mark.parametrize(
|
||||||
c = Color("blue", 0.5)
|
"color, expected",
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
|
[
|
||||||
|
pytest.param(Color(0.0, 1.0, 0.0), (0, 1, 0, 1), id="rgb"),
|
||||||
|
pytest.param(Color(1.0, 1.0, 0.0, 0.5), (1, 1, 0, 0.5), id="rgba"),
|
||||||
|
pytest.param(
|
||||||
|
Color(1.0, 1.0, 0.0, alpha=0.5), (1, 1, 0, 0.5), id="rgb + kw alpha"
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(red=0.1, green=0.2, blue=0.3, alpha=0.5),
|
||||||
|
(0.1, 0.2, 0.3, 0.5),
|
||||||
|
id="kw rgba",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overload_rgba(color, expected):
|
||||||
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
# red + green + blue + alpha overload
|
|
||||||
def test_rgb0(self):
|
|
||||||
c = Color(0.0, 1.0, 0.0)
|
|
||||||
np.testing.assert_allclose(tuple(c), (0, 1, 0, 1), 1e-5)
|
|
||||||
|
|
||||||
def test_rgba1(self):
|
@pytest.mark.parametrize(
|
||||||
c = Color(1.0, 1.0, 0.0, 0.5)
|
"color, expected",
|
||||||
self.assertEqual(c.wrapped.GetRGB().Red(), 1.0)
|
[
|
||||||
self.assertEqual(c.wrapped.GetRGB().Green(), 1.0)
|
pytest.param(
|
||||||
self.assertEqual(c.wrapped.GetRGB().Blue(), 0.0)
|
Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"
|
||||||
self.assertEqual(c.wrapped.Alpha(), 0.5)
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(0x006692, 0x80),
|
||||||
|
(0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF),
|
||||||
|
id="color_code + alpha",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(0x006692, alpha=0x80),
|
||||||
|
(0, 102 / 255, 146 / 255, 128 / 255),
|
||||||
|
id="color_code + kw alpha",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(color_code=0x996692, alpha=0xCC),
|
||||||
|
(153 / 255, 102 / 255, 146 / 255, 204 / 255),
|
||||||
|
id="kw color_code + alpha",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overload_hex(color, expected):
|
||||||
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
def test_rgba2(self):
|
|
||||||
c = Color(1.0, 1.0, 0.0, alpha=0.5)
|
|
||||||
np.testing.assert_allclose(tuple(c), (1, 1, 0, 0.5), 1e-5)
|
|
||||||
|
|
||||||
def test_rgba3(self):
|
@pytest.mark.parametrize(
|
||||||
c = Color(red=0.1, green=0.2, blue=0.3, alpha=0.5)
|
"color, expected",
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.5), 1e-5)
|
[
|
||||||
|
pytest.param(Color((0.1,)), (0.1, 1.0, 1.0, 1.0), id="tuple r"),
|
||||||
|
pytest.param(Color((0.1, 0.2)), (0.1, 0.2, 1.0, 1.0), id="tuple rg"),
|
||||||
|
pytest.param(Color((0.1, 0.2, 0.3)), (0.1, 0.2, 0.3, 1.0), id="tuple rgb"),
|
||||||
|
pytest.param(
|
||||||
|
Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="tuple rbga"
|
||||||
|
),
|
||||||
|
pytest.param(Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="kw tuple"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overload_tuple(color, expected):
|
||||||
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
# hex (int) + alpha overload
|
|
||||||
def test_hex(self):
|
|
||||||
c = Color(0x996692)
|
|
||||||
np.testing.assert_allclose(
|
|
||||||
tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5
|
|
||||||
)
|
|
||||||
|
|
||||||
c = Color(0x006692, 0x80)
|
# ColorLikes
|
||||||
np.testing.assert_allclose(
|
@pytest.mark.parametrize(
|
||||||
tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5
|
"color_like",
|
||||||
)
|
[
|
||||||
|
pytest.param(Quantity_ColorRGBA(1, 0, 0, 1), id="Quantity_ColorRGBA"),
|
||||||
|
pytest.param("red", id="name str"),
|
||||||
|
pytest.param("red ", id="name str whitespace"),
|
||||||
|
pytest.param(("red",), id="tuple name str"),
|
||||||
|
pytest.param(("red", 1), id="tuple name str + alpha"),
|
||||||
|
pytest.param("#ff0000", id="hex str rgb 24bit"),
|
||||||
|
pytest.param(" #ff0000 ", id="hex str rgb 24bit whitespace"),
|
||||||
|
pytest.param(("#ff0000",), id="tuple hex str rgb 24bit"),
|
||||||
|
pytest.param(("#ff0000", 1), id="tuple hex str rgb 24bit + alpha"),
|
||||||
|
pytest.param("#ff0000ff", id="hex str rgba 24bit"),
|
||||||
|
pytest.param(" #ff0000ff ", id="hex str rgba 24bit whitespace"),
|
||||||
|
pytest.param(("#ff0000ff",), id="tuple hex str rgba 24bit"),
|
||||||
|
pytest.param(
|
||||||
|
("#ff0000ff", 0.6), id="tuple hex str rgba 24bit + alpha (not used)"
|
||||||
|
),
|
||||||
|
pytest.param("#f00", id="hex str rgb 12bit"),
|
||||||
|
pytest.param(" #f00 ", id="hex str rgb 12bit whitespace"),
|
||||||
|
pytest.param(("#f00",), id="tuple hex str rgb 12bit"),
|
||||||
|
pytest.param(("#f00", 1), id="tuple hex str rgb 12bit + alpha"),
|
||||||
|
pytest.param("#f00f", id="hex str rgba 12bit"),
|
||||||
|
pytest.param(" #f00f ", id="hex str rgba 12bit whitespace"),
|
||||||
|
pytest.param(("#f00f",), id="tuple hex str rgba 12bit"),
|
||||||
|
pytest.param(("#f00f", 0.6), id="tuple hex str rgba 12bit + alpha (not used)"),
|
||||||
|
pytest.param(0xFF0000, id="hex int"),
|
||||||
|
pytest.param((0xFF0000), id="tuple hex int"),
|
||||||
|
pytest.param((0xFF0000, 0xFF), id="tuple hex int + alpha"),
|
||||||
|
pytest.param((1, 0, 0), id="tuple rgb int"),
|
||||||
|
pytest.param((1, 0, 0, 1), id="tuple rgba int"),
|
||||||
|
pytest.param((1.0, 0.0, 0.0), id="tuple rgb float"),
|
||||||
|
pytest.param((1.0, 0.0, 0.0, 1.0), id="tuple rgba float"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_color_likes(color_like):
|
||||||
|
expected = (1, 0, 0, 1)
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
c = Color(0x006692, alpha=0x80)
|
|
||||||
np.testing.assert_allclose(tuple(c), (0, 102 / 255, 146 / 255, 128 / 255), 1e-5)
|
|
||||||
|
|
||||||
c = Color(color_code=0x996692, alpha=0xCC)
|
@pytest.mark.parametrize(
|
||||||
np.testing.assert_allclose(
|
"color_like, expected",
|
||||||
tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5
|
[
|
||||||
)
|
pytest.param(Color(), (1, 1, 1, 1), id="empty Color()"),
|
||||||
|
pytest.param(1.0, (1, 1, 1, 1), id="r float"),
|
||||||
|
pytest.param((1.0,), (1, 1, 1, 1), id="tuple r float"),
|
||||||
|
pytest.param((1.0, 0.0), (1, 0, 1, 1), id="tuple rg float"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_color_likes_incomplete(color_like, expected):
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
c = Color(0.0, 0.0, 1.0, 1.0)
|
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
|
|
||||||
|
|
||||||
c = Color(0, 0, 1, 1)
|
@pytest.mark.parametrize(
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
|
"color_like",
|
||||||
|
[
|
||||||
|
pytest.param(Quantity_ColorRGBA(1, 0, 0, 0.6), id="Quantity_ColorRGBA"),
|
||||||
|
pytest.param(("red", 0.6), id="tuple name str + alpha"),
|
||||||
|
pytest.param(("#ff0000", 0.6), id="tuple hex str rgb 24bit + alpha"),
|
||||||
|
pytest.param(("#ff000099"), id="tuple hex str rgba 24bit"),
|
||||||
|
pytest.param(("#f00", 0.6), id="tuple hex str rgb 12bit + alpha"),
|
||||||
|
pytest.param(("#f009"), id="tuple hex str rgba 12bit"),
|
||||||
|
pytest.param((0xFF0000, 153), id="tuple hex int + alpha int"),
|
||||||
|
pytest.param((1.0, 0.0, 0.0, 0.6), id="tuple rbga float"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_color_likes_alpha(color_like):
|
||||||
|
expected = (1, 0, 0, 0.6)
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
# Methods
|
|
||||||
def test_to_tuple(self):
|
|
||||||
c = Color("blue", alpha=0.5)
|
|
||||||
np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
|
|
||||||
|
|
||||||
def test_copy(self):
|
# Exceptions
|
||||||
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
@pytest.mark.parametrize(
|
||||||
c_copy = copy.copy(c)
|
"name",
|
||||||
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), 1e-5)
|
[
|
||||||
|
pytest.param("build123d", id="invalid color name"),
|
||||||
|
pytest.param("#ffg", id="invalid rgb 12bit"),
|
||||||
|
pytest.param("#fffg", id="invalid rgba 12bit"),
|
||||||
|
pytest.param("#fffgg", id="invalid rgb 24bit"),
|
||||||
|
pytest.param("#fff00gg", id="invalid rgba 24bit"),
|
||||||
|
pytest.param("#ff", id="short rgb 12bit"),
|
||||||
|
pytest.param("#fffff", id="short rgb 24bit"),
|
||||||
|
pytest.param("#fffffff", id="short rgba 24bit"),
|
||||||
|
pytest.param("#fffffffff", id="long rgba 24bit"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_exceptions_color_name(name):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
Color(name)
|
||||||
|
|
||||||
def test_str_repr(self):
|
|
||||||
c = Color(1, 0, 0)
|
|
||||||
self.assertEqual(str(c), "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'")
|
|
||||||
self.assertEqual(repr(c), "Color(1.0, 0.0, 0.0, 1.0)")
|
|
||||||
|
|
||||||
c = Color(1, .5, 0)
|
@pytest.mark.parametrize(
|
||||||
self.assertEqual(str(c), "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'")
|
"color_type",
|
||||||
self.assertEqual(repr(c), "Color(1.0, 0.5, 0.0, 1.0)")
|
[
|
||||||
|
pytest.param(
|
||||||
|
(
|
||||||
|
dict(
|
||||||
|
{"name": "red", "alpha": 1},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
id="dict arg",
|
||||||
|
),
|
||||||
|
pytest.param(("red", "blue"), id="str + str"),
|
||||||
|
pytest.param((1.0, "blue"), id="float + str order"),
|
||||||
|
pytest.param((1, "blue"), id="int + str order"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_exceptions_color_type(color_type):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
Color(*color_type)
|
||||||
|
|
||||||
def test_tuple(self):
|
|
||||||
c = Color((0.1,))
|
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 1.0, 1.0, 1.0), 1e-5)
|
|
||||||
c = Color((0.1, 0.2))
|
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 1.0, 1.0), 1e-5)
|
|
||||||
c = Color((0.1, 0.2, 0.3))
|
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 1.0), 1e-5)
|
|
||||||
c = Color((0.1, 0.2, 0.3, 0.4))
|
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.4), 1e-5)
|
|
||||||
c = Color(color_like=(0.1, 0.2, 0.3, 0.4))
|
|
||||||
np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.4), 1e-5)
|
|
||||||
|
|
||||||
# color_like overload
|
# Methods
|
||||||
def test_color_like(self):
|
def test_rgba_wrapped():
|
||||||
red_color_likes = [
|
c = Color(1.0, 1.0, 0.0, 0.5)
|
||||||
Quantity_ColorRGBA(1, 0, 0, 1),
|
assert c.wrapped.GetRGB().Red() == 1.0
|
||||||
"red",
|
assert c.wrapped.GetRGB().Green() == 1.0
|
||||||
"red ",
|
assert c.wrapped.GetRGB().Blue() == 0.0
|
||||||
("red",),
|
assert c.wrapped.Alpha() == 0.5
|
||||||
("red", 1),
|
|
||||||
"#ff0000",
|
|
||||||
" #ff0000 ",
|
|
||||||
("#ff0000",),
|
|
||||||
("#ff0000", 1),
|
|
||||||
0xff0000,
|
|
||||||
(0xff0000),
|
|
||||||
(0xff0000, 0xff),
|
|
||||||
(1, 0, 0),
|
|
||||||
(1, 0, 0, 1),
|
|
||||||
(1., 0., 0.),
|
|
||||||
(1., 0., 0., 1.)
|
|
||||||
]
|
|
||||||
expected = (1, 0, 0, 1)
|
|
||||||
for cl in red_color_likes:
|
|
||||||
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
|
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
|
|
||||||
|
|
||||||
incomplete_color_likes = [
|
|
||||||
(Color(), (1, 1, 1, 1)),
|
|
||||||
(1., (1, 1, 1, 1)),
|
|
||||||
((1.,), (1, 1, 1, 1)),
|
|
||||||
((1., 0.), (1, 0, 1, 1)),
|
|
||||||
]
|
|
||||||
for cl, expected in incomplete_color_likes:
|
|
||||||
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
|
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
|
|
||||||
|
|
||||||
alpha_color_likes = [
|
def test_copy():
|
||||||
Quantity_ColorRGBA(1, 0, 0, 0.6),
|
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
||||||
("red", 0.6),
|
c_copy = copy.copy(c)
|
||||||
("#ff0000", 0.6),
|
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
|
||||||
(0xff0000, 153),
|
|
||||||
(1., 0., 0., 0.6)
|
|
||||||
]
|
|
||||||
expected = (1, 0, 0, 0.6)
|
|
||||||
for cl in alpha_color_likes:
|
|
||||||
np.testing.assert_allclose(tuple(Color(cl)), expected, 1e-5)
|
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=cl)), expected, 1e-5)
|
|
||||||
|
|
||||||
# Exceptions
|
|
||||||
def test_bad_color_name(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
Color("build123d")
|
|
||||||
|
|
||||||
def test_bad_color_type(self):
|
def test_str_repr_is():
|
||||||
with self.assertRaises(TypeError):
|
c = Color(1, 0, 0)
|
||||||
Color(dict({"name": "red", "alpha": 1}))
|
assert str(c) == "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'"
|
||||||
|
assert repr(c) == "Color(1.0, 0.0, 0.0, 1.0)"
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
Color("red", "blue")
|
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
def test_str_repr_near():
|
||||||
Color(1., "blue")
|
c = Color(1, 0.5, 0)
|
||||||
|
assert str(c) == "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'"
|
||||||
|
assert repr(c) == "Color(1.0, 0.5, 0.0, 1.0)"
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
Color(1, "blue")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
class TestColorCategoricalSet:
|
||||||
unittest.main()
|
def test_returns_expected_number_of_colors(self):
|
||||||
|
colors = Color.categorical_set(5)
|
||||||
|
assert len(colors) == 5
|
||||||
|
assert all(isinstance(c, Color) for c in colors)
|
||||||
|
|
||||||
|
def test_colors_are_evenly_spaced_in_hue(self):
|
||||||
|
count = 8
|
||||||
|
colors = Color.categorical_set(count)
|
||||||
|
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
|
||||||
|
diffs = [(hues[(i + 1) % count] - hues[i]) % 1.0 for i in range(count)]
|
||||||
|
avg_diff = sum(diffs) / len(diffs)
|
||||||
|
assert all(math.isclose(d, avg_diff, rel_tol=1e-2) for d in diffs)
|
||||||
|
|
||||||
|
def test_starting_hue_as_float(self):
|
||||||
|
(r, g, b, _) = tuple(Color.categorical_set(1, starting_hue=0.25)[0])
|
||||||
|
h = colorsys.rgb_to_hls(r, g, b)[0]
|
||||||
|
assert math.isclose(h, 0.25, rel_tol=0.05)
|
||||||
|
|
||||||
|
def test_starting_hue_as_int_hex(self):
|
||||||
|
# Blue (0x0000FF) should be valid and return a Color
|
||||||
|
c = Color.categorical_set(1, starting_hue=0x0000FF)[0]
|
||||||
|
assert isinstance(c, Color)
|
||||||
|
|
||||||
|
def test_starting_hue_invalid_type(self):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Color.categorical_set(3, starting_hue="invalid")
|
||||||
|
|
||||||
|
def test_starting_hue_out_of_range(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=1.5)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=-0.1)
|
||||||
|
|
||||||
|
def test_starting_hue_negative_int(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=-1)
|
||||||
|
|
||||||
|
def test_constant_alpha_applied(self):
|
||||||
|
colors = Color.categorical_set(3, alpha=0.7)
|
||||||
|
for c in colors:
|
||||||
|
(_, _, _, a) = tuple(c)
|
||||||
|
assert math.isclose(a, 0.7, rel_tol=1e-6)
|
||||||
|
|
||||||
|
def test_iterable_alpha_applied(self):
|
||||||
|
alphas = (0.1, 0.5, 0.9)
|
||||||
|
colors = Color.categorical_set(3, alpha=alphas)
|
||||||
|
for a, c in zip(alphas, colors):
|
||||||
|
(_, _, _, returned_alpha) = tuple(c)
|
||||||
|
assert math.isclose(a, returned_alpha, rel_tol=1e-6)
|
||||||
|
|
||||||
|
def test_iterable_alpha_length_mismatch(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(4, alpha=[0.5, 0.7])
|
||||||
|
|
||||||
|
def test_hues_wrap_around(self):
|
||||||
|
colors = Color.categorical_set(10, starting_hue=0.95)
|
||||||
|
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
|
||||||
|
assert all(0.0 <= h <= 1.0 for h in hues)
|
||||||
|
|
||||||
|
def test_alpha_defaults_to_one(self):
|
||||||
|
colors = Color.categorical_set(4)
|
||||||
|
for c in colors:
|
||||||
|
(_, _, _, a) = tuple(c)
|
||||||
|
assert math.isclose(a, 1.0, rel_tol=1e-6)
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
|
||||||
distance=1, distance2=2, vertices=[vertex], edge=other_edge
|
distance=1, distance2=2, vertices=[vertex], edge=other_edge
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_make_rect(self):
|
def test_plane_as_face(self):
|
||||||
test_face = Face.make_plane()
|
test_face = Face(Plane.XY)
|
||||||
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
|
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
|
||||||
|
|
||||||
def test_length_width(self):
|
def test_length_width(self):
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ def test_shape_0d(obj, target, expected):
|
||||||
run_test(obj, target, expected)
|
run_test(obj, target, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# 1d Shapes
|
||||||
ed1 = Line((0, 0), (5, 0)).edge()
|
ed1 = Line((0, 0), (5, 0)).edge()
|
||||||
ed2 = Line((0, -1), (5, 1)).edge()
|
ed2 = Line((0, -1), (5, 1)).edge()
|
||||||
ed3 = Line((0, 0, 5), (5, 0, 5)).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)
|
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
|
# FreeCAD issue example
|
||||||
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
c1 = CenterArc((0, 0), 10, 0, 360).edge()
|
||||||
c2 = CenterArc((19, 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(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
|
||||||
Case(horz, e1, [Vertex], "horizontal, 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(c1, horz, [Vertex], "circle, horiz, tangent", None),
|
||||||
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
|
||||||
Case(c1, vert, [Vertex], "circle, vert, 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))
|
f1 = Face(Wire.make_circle(0.5))
|
||||||
|
|
||||||
issues_matrix = [
|
issues_matrix = [
|
||||||
Case(t, t, [Face, Face], "issue #1015", "Returns Compound"),
|
Case(t, t, [Face, Face], "issue #1015", None),
|
||||||
Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"),
|
Case(l, s, [Edge], "issue #945", None),
|
||||||
Case(a, b, [Edge], "issue #918", "Returns empty Compound"),
|
Case(a, b, [Edge], "issue #918", None),
|
||||||
Case(e1, w1, [Vertex, Vertex], "issue #697"),
|
Case(e1, w1, [Vertex, Vertex], "issue #697", None),
|
||||||
Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"),
|
Case(e1, f1, [Edge], "issue #697", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
|
||||||
|
|
@ -279,6 +469,9 @@ def test_issues(obj, target, expected):
|
||||||
exception_matrix = [
|
exception_matrix = [
|
||||||
Case(vt1, Color(), None, "Unsupported type", None),
|
Case(vt1, Color(), None, "Unsupported type", None),
|
||||||
Case(ed1, 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
|
@pytest.mark.skip
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ license:
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from build123d.build_enums import (
|
from build123d.build_enums import (
|
||||||
CenterOf,
|
CenterOf,
|
||||||
|
|
@ -106,13 +107,73 @@ class TestMixin1D(unittest.TestCase):
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_positions(self):
|
def test_positions_with_distances(self):
|
||||||
e = Edge.make_line((0, 0, 0), (1, 1, 1))
|
e = Edge.make_line((0, 0, 0), (1, 1, 1))
|
||||||
distances = [i / 4 for i in range(3)]
|
distances = [i / 4 for i in range(3)]
|
||||||
pts = e.positions(distances)
|
pts = e.positions(distances)
|
||||||
for i, position in enumerate(pts):
|
for i, position in enumerate(pts):
|
||||||
self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5)
|
self.assertAlmostEqual(position, (i / 4, i / 4, i / 4), 5)
|
||||||
|
|
||||||
|
def test_positions_deflection_line(self):
|
||||||
|
"""Deflection sampling on a straight line should yield exactly 2 points."""
|
||||||
|
e = Edge.make_line((0, 0, 0), (10, 0, 0))
|
||||||
|
pts = e.positions(deflection=0.1)
|
||||||
|
|
||||||
|
self.assertEqual(len(pts), 2)
|
||||||
|
self.assertAlmostEqual(pts[0], (0, 0, 0), 7)
|
||||||
|
self.assertAlmostEqual(pts[1], (10, 0, 0), 7)
|
||||||
|
|
||||||
|
def test_positions_deflection_circle(self):
|
||||||
|
"""Deflection on a C2 curve (circle) should produce multiple points."""
|
||||||
|
radius = 5
|
||||||
|
e = Edge.make_circle(radius)
|
||||||
|
|
||||||
|
pts = e.positions(deflection=0.1)
|
||||||
|
|
||||||
|
# Should produce more than just two points
|
||||||
|
self.assertGreater(len(pts), 2)
|
||||||
|
|
||||||
|
# Endpoints should match curve endpoints
|
||||||
|
first, last = pts[0], pts[-1]
|
||||||
|
curve = e.geom_adaptor()
|
||||||
|
p0 = Vector(curve.Value(curve.FirstParameter()))
|
||||||
|
p1 = Vector(curve.Value(curve.LastParameter()))
|
||||||
|
|
||||||
|
self.assertAlmostEqual(first, p0, 7)
|
||||||
|
self.assertAlmostEqual(last, p1, 7)
|
||||||
|
|
||||||
|
def test_positions_deflection_resolution(self):
|
||||||
|
"""Smaller deflection tolerance should produce more points."""
|
||||||
|
e = Edge.make_circle(10)
|
||||||
|
|
||||||
|
pts_coarse = e.positions(deflection=0.5)
|
||||||
|
pts_fine = e.positions(deflection=0.05)
|
||||||
|
|
||||||
|
self.assertGreater(len(pts_fine), len(pts_coarse))
|
||||||
|
|
||||||
|
def test_positions_deflection_C0_curve(self):
|
||||||
|
"""C0 spline should use QuasiUniformDeflection and still succeed."""
|
||||||
|
e = Polyline((0, 0), (1, 2), (2, 0))._to_bspline() # C0
|
||||||
|
pts = e.positions(deflection=0.1)
|
||||||
|
|
||||||
|
self.assertGreater(len(pts), 2)
|
||||||
|
|
||||||
|
def test_positions_missing_arguments(self):
|
||||||
|
e = Edge.make_line((0, 0, 0), (1, 0, 0))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
e.positions()
|
||||||
|
|
||||||
|
def test_positions_deflection_failure(self):
|
||||||
|
e = Edge.make_circle(1.0)
|
||||||
|
|
||||||
|
with patch("build123d.topology.one_d.GCPnts_UniformDeflection") as MockDefl:
|
||||||
|
instance = MockDefl.return_value
|
||||||
|
instance.IsDone.return_value = False
|
||||||
|
instance.NbPoints.return_value = 0
|
||||||
|
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
e.positions(deflection=0.1)
|
||||||
|
|
||||||
def test_tangent_at(self):
|
def test_tangent_at(self):
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0),
|
Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(1.0),
|
||||||
|
|
|
||||||
|
|
@ -229,13 +229,15 @@ class TestOrientedBoundBox(unittest.TestCase):
|
||||||
obb = OrientedBoundBox(rect)
|
obb = OrientedBoundBox(rect)
|
||||||
corners = obb.corners
|
corners = obb.corners
|
||||||
poly = Polygon(*corners, align=None)
|
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():
|
for face in Box(1, 2, 3).faces():
|
||||||
obb = OrientedBoundBox(face)
|
obb = OrientedBoundBox(face)
|
||||||
corners = obb.corners
|
corners = obb.corners
|
||||||
poly = Polygon(*corners, align=None)
|
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):
|
def test_line_corners(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,11 @@ class TestShape(unittest.TestCase):
|
||||||
self.assertEqual(len(top), 2)
|
self.assertEqual(len(top), 2)
|
||||||
self.assertAlmostEqual(top[0].length, 3, 5)
|
self.assertAlmostEqual(top[0].length, 3, 5)
|
||||||
|
|
||||||
def test_split_return_none(self):
|
def test_split_invalid_keep(self):
|
||||||
shape = Box(1, 1, 1) - Pos((0, 0, -0.25)) * Box(1, 0.5, 0.5)
|
with self.assertRaises(ValueError):
|
||||||
split_shape = shape.split(Plane.XY, keep=Keep.INSIDE)
|
Box(1, 1, 1).split(Plane.XY, keep=Keep.INSIDE)
|
||||||
self.assertIsNone(split_shape)
|
with self.assertRaises(ValueError):
|
||||||
|
Box(1, 1, 1).split(Plane.XY, keep=Keep.OUTSIDE)
|
||||||
|
|
||||||
def test_split_by_perimeter(self):
|
def test_split_by_perimeter(self):
|
||||||
# Test 0 - extract a spherical cap
|
# Test 0 - extract a spherical cap
|
||||||
|
|
@ -299,7 +300,8 @@ class TestShape(unittest.TestCase):
|
||||||
predicted_location = Location(offset) * Rotation(*rotation)
|
predicted_location = Location(offset) * Rotation(*rotation)
|
||||||
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
|
||||||
intersect = shape.intersect(located_shape)
|
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):
|
def test_position_and_orientation(self):
|
||||||
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
|
||||||
|
|
@ -475,7 +477,7 @@ class TestShape(unittest.TestCase):
|
||||||
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
||||||
self.assertListEqual(edges, [])
|
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.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
|
||||||
self.assertListEqual(edges, [])
|
self.assertListEqual(edges, [])
|
||||||
|
|
||||||
|
|
@ -493,7 +495,7 @@ class TestShape(unittest.TestCase):
|
||||||
self.assertEqual(len(edges1), 1)
|
self.assertEqual(len(edges1), 1)
|
||||||
self.assertAlmostEqual(edges1[0].length, 20, 5)
|
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(vertices2), 1)
|
||||||
self.assertEqual(len(edges2), 1)
|
self.assertEqual(len(edges2), 1)
|
||||||
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
|
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
|
||||||
|
|
@ -588,7 +590,7 @@ class TestShape(unittest.TestCase):
|
||||||
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
empty.distance_to_with_closest_points(Vector(1, 1, 1))
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
empty.distance_to(Vector(1, 1, 1))
|
empty.distance_to(Vector(1, 1, 1))
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(AttributeError):
|
||||||
box.intersect(empty_loc)
|
box.intersect(empty_loc)
|
||||||
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
|
||||||
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,9 @@ class TestSolid(unittest.TestCase):
|
||||||
self.assertAlmostEqual(twist.volume, 1, 5)
|
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
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
|
# Wire
|
||||||
base = Wire.make_rect(1, 1)
|
base = Wire.make_rect(1, 1)
|
||||||
twist = Solid.extrude_linear_with_rotation(
|
twist = Solid.extrude_linear_with_rotation(
|
||||||
|
|
@ -162,7 +164,9 @@ class TestSolid(unittest.TestCase):
|
||||||
self.assertAlmostEqual(twist.volume, 1, 5)
|
self.assertAlmostEqual(twist.volume, 1, 5)
|
||||||
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
|
||||||
bottom = twist.faces().sort_by(Axis.Z)[0]
|
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):
|
def test_make_loft(self):
|
||||||
loft = Solid.make_loft(
|
loft = Solid.make_loft(
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,8 @@ class DimensionLineTestCase(unittest.TestCase):
|
||||||
],
|
],
|
||||||
draft=metric,
|
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):
|
def test_outside_arrows(self):
|
||||||
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
|
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from io import BytesIO
|
||||||
from os import fsdecode, fsencode
|
from os import fsdecode, fsencode
|
||||||
from typing import Union, Iterable
|
from typing import Union, Iterable
|
||||||
import math
|
import math
|
||||||
|
|
@ -194,7 +195,9 @@ class ExportersTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"format", (Path, fsencode, fsdecode), ids=["path", "bytes", "str"]
|
"format",
|
||||||
|
(Path, fsencode, fsdecode),
|
||||||
|
ids=["path", "bytes", "str"],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
|
@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
|
||||||
def test_pathlike_exporters(tmp_path, format, Exporter):
|
def test_pathlike_exporters(tmp_path, format, Exporter):
|
||||||
|
|
@ -205,5 +208,14 @@ def test_pathlike_exporters(tmp_path, format, Exporter):
|
||||||
exporter.write(path)
|
exporter.write(path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
|
||||||
|
def test_exporters_in_memory(Exporter):
|
||||||
|
buffer = BytesIO()
|
||||||
|
sketch = ExportersTestCase.create_test_sketch()
|
||||||
|
exporter = Exporter()
|
||||||
|
exporter.add_shape(sketch)
|
||||||
|
exporter.write(buffer)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,11 @@ def test_pathlike_exporters(tmp_path, format, exporter):
|
||||||
exporter(box, path)
|
exporter(box, path)
|
||||||
|
|
||||||
|
|
||||||
def test_export_brep_in_memory():
|
@pytest.mark.parametrize("exporter", (export_step, export_brep))
|
||||||
|
def test_exporters_in_memory(exporter):
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
box = Box(1, 1, 1).locate(Pos(-1, -2, -3))
|
box = Box(1, 1, 1).locate(Pos(-1, -2, -3))
|
||||||
export_brep(box, buffer)
|
exporter(box, buffer)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import unittest, uuid
|
import unittest, uuid
|
||||||
|
from io import BytesIO
|
||||||
from packaging.specifiers import SpecifierSet
|
from packaging.specifiers import SpecifierSet
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from os import fsdecode, fsencode
|
from os import fsdecode, fsencode
|
||||||
|
|
@ -209,6 +210,7 @@ class TestHollowImport(unittest.TestCase):
|
||||||
importer = Mesher()
|
importer = Mesher()
|
||||||
stl = importer.read("test.stl")
|
stl = importer.read("test.stl")
|
||||||
self.assertTrue(stl[0].is_valid)
|
self.assertTrue(stl[0].is_valid)
|
||||||
|
self.assertAlmostEqual(test_shape.volume, stl[0].volume, 0)
|
||||||
|
|
||||||
|
|
||||||
class TestImportDegenerateTriangles(unittest.TestCase):
|
class TestImportDegenerateTriangles(unittest.TestCase):
|
||||||
|
|
@ -237,5 +239,13 @@ def test_pathlike_mesher(tmp_path, format):
|
||||||
importer.read(path)
|
importer.read(path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("file_type", ("3mf", "stl"))
|
||||||
|
def test_in_memory_mesher(file_type):
|
||||||
|
stream = BytesIO()
|
||||||
|
exporter = Mesher()
|
||||||
|
exporter.add_shape(Solid.make_box(1, 1, 1))
|
||||||
|
exporter.write_stream(stream, file_type)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue